import React, { createContext, useContext, useState, useCallback, ReactNode, useEffect } from 'react'; import { CheckCircle2, AlertCircle, Info, XCircle, X } from 'lucide-react'; export type ToastVariant = 'success' | 'info' | 'warning' | 'error'; export interface ToastMessage { id: string; message: React.ReactNode; variant?: ToastVariant; duration?: number; isActionable?: boolean; } interface ToastContextType { addToast: (toast: Omit) => void; removeToast: (id: string) => void; } const ToastContext = createContext(undefined); export const ToastProvider: React.FC<{ children: ReactNode }> = ({ children }) => { const [toasts, setToasts] = useState([]); const addToast = useCallback((toast: Omit) => { setToasts(prev => [...prev, { ...toast, id: Math.random().toString(36).substring(2, 9) }]); }, []); const removeToast = useCallback((id: string) => { setToasts(prev => prev.map(t => t.id === id ? { ...t, exiting: true } : t) as any); setTimeout(() => { setToasts(prev => prev.filter(t => t.id !== id)); }, 300); // Matches the exit animation duration }, []); return ( {children} ); }; export const useToast = () => { const context = useContext(ToastContext); if (!context) throw new Error('useToast must be used within ToastProvider'); return context; }; const ToastItem: React.FC<{ toast: ToastMessage & { exiting?: boolean }; removeToast: (id: string) => void }> = ({ toast, removeToast }) => { const [isHovered, setIsHovered] = useState(false); useEffect(() => { let timeoutDuration = toast.duration ?? 5000; if (timeoutDuration < 5000) timeoutDuration = 5000; if (toast.isActionable || (toast.variant === 'error' && !toast.duration)) { return; } if (isHovered) { return; } const timer = setTimeout(() => { removeToast(toast.id); }, timeoutDuration); return () => clearTimeout(timer); }, [toast, isHovered, removeToast]); const role = toast.variant === 'error' ? 'alert' : 'status'; const variantStyles = { success: 'border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 shadow-emerald-500/10', info: 'border-blue-500/30 bg-blue-500/10 text-blue-600 dark:text-blue-400 shadow-blue-500/10', warning: 'border-amber-500/30 bg-amber-500/10 text-amber-600 dark:text-amber-400 shadow-amber-500/10', error: 'border-red-500/30 bg-red-500/10 text-red-600 dark:text-red-400 shadow-red-500/10', }; const icons = { success: , error: , warning: , info: , }; const variant = toast.variant || 'info'; const style = variantStyles[variant]; const Icon = icons[variant]; return (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} onFocus={() => setIsHovered(true)} onBlur={() => setIsHovered(false)} >
{Icon}
{toast.message}
); }; const ToastContainer: React.FC<{ toasts: ToastMessage[]; removeToast: (id: string) => void }> = ({ toasts, removeToast }) => { return (
{toasts.map(toast => ( ))}
); };