import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react'; export type ToastLevel = 'info' | 'success' | 'warn' | 'error'; export interface Toast { id: string; level: ToastLevel; message: string; detail?: string; createdAt: number; durationMs: number; } export interface ShowToastOptions { level?: ToastLevel; detail?: string; /** Override default auto-dismiss; 0 = stay until dismissed */ durationMs?: number; } export const TOAST_DURATION_MS: Record = { info: 5000, success: 5000, warn: 6000, error: 10000, }; export const MAX_VISIBLE_TOASTS = 5; let toastCounter = 0; function nextToastId(): string { toastCounter += 1; return `toast-${toastCounter}`; } export interface ToastContextValue { toasts: Toast[]; showToast: (message: string, options?: ShowToastOptions) => string; dismissToast: (id: string) => void; clearAllToasts: () => void; } const ToastContext = createContext({ toasts: [], showToast: () => '', dismissToast: () => {}, clearAllToasts: () => {}, }); export function ToastProvider({ children }: { children: React.ReactNode }) { const [toasts, setToasts] = useState([]); const timersRef = useRef>>(new Map()); const dismissToast = useCallback((id: string) => { const timer = timersRef.current.get(id); if (timer) { clearTimeout(timer); timersRef.current.delete(id); } setToasts((prev) => prev.filter((t) => t.id !== id)); }, []); const clearAllToasts = useCallback(() => { for (const timer of timersRef.current.values()) clearTimeout(timer); timersRef.current.clear(); setToasts([]); }, []); const scheduleDismiss = useCallback( (toast: Toast) => { if (toast.durationMs <= 0) return; const timer = setTimeout(() => dismissToast(toast.id), toast.durationMs); timersRef.current.set(toast.id, timer); }, [dismissToast], ); const showToast = useCallback( (message: string, options?: ShowToastOptions) => { const level = options?.level ?? 'info'; const durationMs = options?.durationMs ?? TOAST_DURATION_MS[level]; const toast: Toast = { id: nextToastId(), level, message, detail: options?.detail, createdAt: Date.now(), durationMs, }; setToasts((prev) => [toast, ...prev].slice(0, MAX_VISIBLE_TOASTS)); scheduleDismiss(toast); return toast.id; }, [scheduleDismiss], ); useEffect(() => { const timers = timersRef.current; return () => { for (const timer of timers.values()) clearTimeout(timer); timers.clear(); }; }, []); const value = useMemo( () => ({ toasts, showToast, dismissToast, clearAllToasts }), [toasts, showToast, dismissToast, clearAllToasts], ); return {children}; } export function useToast(): ToastContextValue { return useContext(ToastContext); }