import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from 'react'; import { createPortal } from 'react-dom'; import { CheckCircle2, CircleAlert, X } from 'lucide-react'; type Toast = { id: number; title: string; message?: string; variant?: 'success' | 'error'; entering?: boolean; closing?: boolean; }; type ToastContextValue = { showToast: (toast: Omit) => void; }; const ToastContext = createContext(null); const TOAST_DURATION_MS = 2800; const TOAST_EXIT_MS = 180; export function ToastProvider({ children }: { children: ReactNode }) { const [toasts, setToasts] = useState([]); const removeToast = useCallback((id: number) => { setToasts((current) => current.map((toast) => (toast.id === id ? { ...toast, closing: true } : toast))); window.setTimeout(() => { setToasts((current) => current.filter((toast) => toast.id !== id)); }, TOAST_EXIT_MS); }, []); const showToast = useCallback((toast: Omit) => { const id = Date.now() + Math.floor(Math.random() * 1000); setToasts((current) => [...current, { ...toast, id, entering: true }]); window.setTimeout(() => { setToasts((current) => current.map((item) => (item.id === id ? { ...item, entering: false } : item))); }, 20); }, []); const value = useMemo(() => ({ showToast }), [showToast]); return ( {children} {createPortal(
{toasts.map((toast) => ( removeToast(toast.id)} /> ))}
, document.body, )}
); } export function useToast() { const context = useContext(ToastContext); if (!context) throw new Error('useToast must be used within ToastProvider'); return context; } function ToastItem({ toast, onClose }: { toast: Toast; onClose: () => void }) { useEffect(() => { const timer = window.setTimeout(onClose, TOAST_DURATION_MS); return () => window.clearTimeout(timer); }, [onClose]); const Icon = toast.variant === 'error' ? CircleAlert : CheckCircle2; const iconTone = toast.variant === 'error' ? 'bg-[#e11d48] text-white' : 'bg-[#4f46e5] text-white'; return (

{toast.title}

{toast.message ?

{toast.message}

: null}
); }