"use client"; import { useEffect, useState, useCallback, useRef } from "react"; import { createPortal } from "react-dom"; import { X, Check, AlertCircle, Info, AlertTriangle } from "lucide-react"; import { cn } from "@/lib/utils"; export type ToastType = "success" | "error" | "info" | "warning"; export interface ToastAction { label: string; onClick: () => void; } export interface Toast { id: string; type: ToastType; title: string; message?: string; duration?: number; onClick?: () => void; icon?: React.ReactNode; action?: ToastAction; } interface ToastProps { toast: Toast; onClose: (id: string) => void; } const icons = { success: Check, error: AlertCircle, info: Info, warning: AlertTriangle, }; const iconContainerStyles = { success: "bg-emerald-500 text-white", error: "bg-red-500 text-white", info: "bg-blue-500 text-white", warning: "bg-amber-500 text-white", }; const progressBarStyles = { success: "bg-emerald-500", error: "bg-red-500", info: "bg-blue-500", warning: "bg-amber-500", }; export function ToastItem({ toast, onClose }: ToastProps) { const Icon = icons[toast.type]; const [exiting, setExiting] = useState(false); const [paused, setPaused] = useState(false); const remainingRef = useRef(toast.duration ?? 5000); const startRef = useRef(Date.now()); const timerRef = useRef | null>(null); const dismiss = useCallback(() => { setExiting(true); setTimeout(() => onClose(toast.id), 280); }, [onClose, toast.id]); useEffect(() => { if (!toast.duration || toast.duration <= 0) return; if (paused) { if (timerRef.current) clearTimeout(timerRef.current); remainingRef.current = remainingRef.current - (Date.now() - startRef.current); return; } startRef.current = Date.now(); timerRef.current = setTimeout(dismiss, remainingRef.current); return () => { if (timerRef.current) clearTimeout(timerRef.current); }; }, [toast.duration, paused, dismiss]); return (
setPaused(true)} onMouseLeave={() => setPaused(false)} onClick={() => { if (toast.onClick && !toast.action) { toast.onClick(); dismiss(); } }} > {/* Left accent bar */}
{/* Icon */} {toast.icon !== undefined ? ( toast.icon ) : (
)} {/* Content */}

{toast.title}

{toast.message && (

{toast.message}

)} {toast.action && ( )}
{/* Close button */}
{/* Progress bar */} {toast.duration && toast.duration > 0 && (
)}
); } export function ToastContainer({ toasts, onClose }: { toasts: Toast[]; onClose: (id: string) => void }) { const [mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); }, []); if (!mounted) return null; return createPortal(
{toasts.map((toast) => (
))}
, document.body ); }