import { createContext, useContext, useState, useCallback, useRef, useEffect, ReactNode, } from 'react'; interface ToastOptions { type?: 'success' | 'error' | 'info' | 'warning'; message: string; title?: string; icon?: ReactNode; duration?: number; onClick?: () => void; } interface Toast extends ToastOptions { id: number; } interface ToastContextType { showToast: (options: ToastOptions) => void; } const ToastContext = createContext(null); const MAX_VISIBLE = 3; // eslint-disable-next-line react-refresh/only-export-components export function useToast() { const context = useContext(ToastContext); if (!context) { throw new Error('useToast must be used within ToastProvider'); } return context; } export function ToastProvider({ children }: { children: ReactNode }) { const [toasts, setToasts] = useState([]); const timersRef = useRef>>(new Map()); const removeToast = useCallback((id: number) => { const timer = timersRef.current.get(id); if (timer) { clearTimeout(timer); timersRef.current.delete(id); } setToasts((prev) => prev.filter((t) => t.id !== id)); }, []); const showToast = useCallback( (options: ToastOptions) => { const id = Date.now() + Math.random(); const toast: Toast = { id, duration: 5000, type: 'info', ...options }; setToasts((prev) => { const next = [...prev, toast]; // Evict oldest toasts beyond the limit if (next.length > MAX_VISIBLE) { const evicted = next.slice(0, next.length - MAX_VISIBLE); for (const old of evicted) { const timer = timersRef.current.get(old.id); if (timer) { clearTimeout(timer); timersRef.current.delete(old.id); } } return next.slice(-MAX_VISIBLE); } return next; }); const timer = setTimeout(() => { removeToast(id); }, toast.duration); timersRef.current.set(id, timer); }, [removeToast], ); // Cleanup all timers on unmount useEffect(() => { const timers = timersRef.current; return () => { timers.forEach((timer) => clearTimeout(timer)); timers.clear(); }; }, []); return ( {children} {/* Toast Container — safe area aware, adaptive width */}
{toasts.map((toast) => ( removeToast(toast.id)} /> ))}
); } function ToastItem({ toast, onClose }: { toast: Toast; onClose: () => void }) { const handleClick = () => { toast.onClick?.(); onClose(); }; const typeStyles = { success: { border: 'border-l-success-500', icon: 'text-success-400', iconBg: 'bg-success-500/20', progress: 'bg-success-400', }, error: { border: 'border-l-error-500', icon: 'text-error-400', iconBg: 'bg-error-500/20', progress: 'bg-error-400', }, warning: { border: 'border-l-warning-500', icon: 'text-warning-400', iconBg: 'bg-warning-500/20', progress: 'bg-warning-400', }, info: { border: 'border-l-accent-500', icon: 'text-accent-400', iconBg: 'bg-accent-500/20', progress: 'bg-accent-400', }, }; const style = typeStyles[toast.type || 'info']; const defaultIcons = { success: ( ), error: ( ), warning: ( ), info: ( ), }; return (
{/* Icon */}
{toast.icon || defaultIcons[toast.type || 'info']}
{/* Content */}
{toast.title && (

{toast.title}

)}

{toast.message}

{/* Progress bar */}
); }