| 57 | } as const; |
| 58 | |
| 59 | export function Toast({ toast, onDismiss }: ToastProps) { |
| 60 | const [paused, setPaused] = useState(false); |
| 61 | const [expanded, setExpanded] = useState(false); |
| 62 | const [progress, setProgress] = useState(100); |
| 63 | |
| 64 | // Track remaining time across pause/resume cycles |
| 65 | const remainingRef = useRef(toast.duration); |
| 66 | |
| 67 | const dismiss = useCallback(() => onDismiss(toast.id), [onDismiss, toast.id]); |
| 68 | |
| 69 | useEffect(() => { |
| 70 | if (toast.duration === 0) return; // loading: never auto-dismiss |
| 71 | if (paused) return; |
| 72 | |
| 73 | const snapRemaining = remainingRef.current; |
| 74 | const start = Date.now(); |
| 75 | |
| 76 | const interval = setInterval(() => { |
| 77 | const elapsed = Date.now() - start; |
| 78 | const newRemaining = Math.max(0, snapRemaining - elapsed); |
| 79 | remainingRef.current = newRemaining; |
| 80 | setProgress((newRemaining / toast.duration) * 100); |
| 81 | |
| 82 | if (newRemaining === 0) { |
| 83 | clearInterval(interval); |
| 84 | dismiss(); |
| 85 | } |
| 86 | }, 50); |
| 87 | |
| 88 | return () => clearInterval(interval); |
| 89 | }, [paused, toast.duration, dismiss]); |
| 90 | |
| 91 | const config = VARIANT_CONFIG[toast.variant]; |
| 92 | const Icon = config.icon; |
| 93 | |
| 94 | return ( |
| 95 | <div |
| 96 | className={cn( |
| 97 | "relative flex flex-col rounded-lg shadow-xl border overflow-hidden", |
| 98 | "w-80 pointer-events-auto backdrop-blur-sm", |
| 99 | config.border, |
| 100 | config.bg |
| 101 | )} |
| 102 | onMouseEnter={() => setPaused(true)} |
| 103 | onMouseLeave={() => setPaused(false)} |
| 104 | > |
| 105 | <div className="flex items-start gap-3 p-3.5 pb-5"> |
| 106 | {/* Icon */} |
| 107 | <div className={cn("mt-0.5 shrink-0", config.iconColor)}> |
| 108 | <Icon |
| 109 | className={cn("w-4 h-4", toast.variant === "loading" && "animate-spin")} |
| 110 | /> |
| 111 | </div> |
| 112 | |
| 113 | {/* Content */} |
| 114 | <div className="flex-1 min-w-0"> |
| 115 | <p className="text-sm font-medium text-surface-100 leading-snug"> |
| 116 | {toast.title} |