'use client'; import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react'; import { clsx } from 'clsx'; import { twMerge } from 'tailwind-merge'; /* ---------- Types ---------- */ export type ToastVariant = 'success' | 'error' | 'info' | 'loading'; export interface ToastItem { id: string; variant: ToastVariant; message: string; duration?: number; // ms — 0 = manual dismiss } export interface ToastAPI { toast: (variant: ToastVariant, message: string, duration?: number) => string; dismiss: (id: string) => void; dismissAll: () => void; } /* ---------- Context ---------- */ const ToastContext = createContext(null); export function useToast(): ToastAPI { const ctx = useContext(ToastContext); if (!ctx) throw new Error('useToast must be used within '); return ctx; } /* ---------- Icons ---------- */ const CheckIcon: React.FC = () => ( ); const ErrorIcon: React.FC = () => ( ); const InfoIcon: React.FC = () => ( ); const LoadingIcon: React.FC = () => ( ); const icons: Record = { success: CheckIcon, error: ErrorIcon, info: InfoIcon, loading: LoadingIcon, }; const borderColors: Record = { success: 'border-l-emerald-500', error: 'border-l-red-500', info: 'border-l-blue-500', loading: 'border-l-gray-500', }; const defaultDurations: Record = { success: 5000, error: 8000, info: 5000, loading: 0, // manual dismiss }; /* ---------- Single Toast ---------- */ interface ToastCardProps { item: ToastItem; onDismiss: (id: string) => void; className?: string; } const ToastCard: React.FC = ({ item, onDismiss, className }) => { const [exiting, setExiting] = useState(false); const timerRef = useRef>(undefined); const Icon = icons[item.variant]; const duration = item.duration ?? defaultDurations[item.variant]; useEffect(() => { if (duration > 0) { timerRef.current = setTimeout(() => { setExiting(true); setTimeout(() => onDismiss(item.id), 200); }, duration); } return () => clearTimeout(timerRef.current); }, [duration, item.id, onDismiss]); return (

{item.message}

); }; /* ---------- Provider ---------- */ export interface ToastProviderProps { children: React.ReactNode; className?: string; } let globalId = 0; export const ToastProvider: React.FC = ({ children, className }) => { const [toasts, setToasts] = useState([]); const dismiss = useCallback((id: string) => { setToasts((prev) => prev.filter((t) => t.id !== id)); }, []); const dismissAll = useCallback(() => setToasts([]), []); const toast = useCallback( (variant: ToastVariant, message: string, duration?: number): string => { const id = `toast-${++globalId}`; setToasts((prev) => [...prev, { id, variant, message, duration }]); return id; }, [], ); return ( {children} {/* Toast container — bottom-right */}
{toasts.map((t) => ( ))}
); }; ToastProvider.displayName = 'ToastProvider';