import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode, } from "react"; function cx(...parts: Array): string { return parts.filter(Boolean).join(" "); } type Variant = "success" | "error" | "info"; interface Toast { id: string; variant: Variant; message: string; /** ms before auto-dismiss; errors default to longer. */ duration?: number; } interface ToastContextValue { notify: (t: Omit) => void; } const ToastContext = createContext(null); export function useToast(): ToastContextValue { const ctx = useContext(ToastContext); if (!ctx) throw new Error("useToast must be used within "); return ctx; } const styles: Record = { success: { ring: "border-emerald-500/30 text-emerald-700 dark:text-emerald-300", role: "status", live: "polite", icon: , }, error: { ring: "border-red-500/30 text-red-700 dark:text-red-300", role: "alert", live: "assertive", icon: , }, info: { ring: "border-indigo-500/30 text-indigo-700 dark:text-indigo-300", role: "status", live: "polite", icon: , }, }; export function ToastProvider({ children }: { children: ReactNode }) { const [toasts, setToasts] = useState([]); const dismiss = useCallback((id: string) => { setToasts((cur) => cur.filter((t) => t.id !== id)); }, []); const notify = useCallback((t: Omit) => { const id = crypto.randomUUID(); // Cap at 3 simultaneous toasts, newest last. setToasts((cur) => [...cur, { ...t, id }].slice(-3)); }, []); const value = useMemo(() => ({ notify }), [notify]); return ( {children} {/* Region exists before toasts render so screen readers announce updates. */}
{toasts.map((t) => ( dismiss(t.id)} /> ))}
); } function ToastItem({ toast, onDismiss }: { toast: Toast; onDismiss: () => void }) { const s = styles[toast.variant]; const duration = toast.duration ?? (toast.variant === "error" ? 8000 : 5000); const [leaving, setLeaving] = useState(false); const timer = useRef(null); const remaining = useRef(duration); const startedAt = useRef(0); const beginExit = useCallback(() => { setLeaving(true); // Wait for the exit transition before removing from the DOM. window.setTimeout(onDismiss, 150); }, [onDismiss]); const clear = useCallback(() => { if (timer.current) window.clearTimeout(timer.current); timer.current = null; }, []); const resume = useCallback(() => { clear(); startedAt.current = Date.now(); timer.current = window.setTimeout(beginExit, remaining.current); }, [beginExit, clear]); const pause = useCallback(() => { clear(); remaining.current -= Date.now() - startedAt.current; }, [clear]); useEffect(() => { resume(); return clear; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return (

{toast.message}

{/* @keyframes toastIn { from { opacity:0; transform: translateY(8px) } to { opacity:1; transform:none } } — define in global CSS */}
); } /* Usage: const { notify } = useToast(); notify({ variant: "success", message: "Order placed." }); notify({ variant: "error", message: "Payment failed. Try again." }); */