'use client'; import React, { useEffect, useState } from 'react'; import { ToastProps, RichColorsMode, ToastSize } from '../types'; import { useToastContext } from '../context/ToastContext'; import { DEFAULT_DURATION } from '../utils/constants'; import { X, CheckCircle, AlertCircle, Info, AlertTriangle, Loader2 } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import ToastProgressBar from '../utils/ToastProgressBar'; interface ToastComponentProps extends ToastProps { isExpanded: boolean; isStacked: boolean; showCloseButton?: boolean; showProgressBar?: boolean; richColors?: RichColorsMode; size?: ToastSize; } export const Toast: React.FC = ({ id, content, description, type, status, duration = DEFAULT_DURATION, icon, actions, input, onClose, onClick, progress, customStyles, className = '', isExpanded, isStacked, showCloseButton, showProgressBar, size = 'md', priority = 'normal', expandable = false, expanded: controlledExpanded, customComponent, unstyled = false, cancel, richColors, }) => { const { removeToast, updateToast } = useToastContext(); const [inputValue, setInputValue] = useState(''); const [localProgress, setLocalProgress] = useState(progress || 100); const [isPaused, setIsPaused] = useState(false); const [internalExpanded, setInternalExpanded] = useState(false); const startTimeRef = React.useRef(Date.now()); const elapsedWhenPausedRef = React.useRef(0); const animationFrameRef = React.useRef(null); const isInitialMountRef = React.useRef(true); const removeTimeoutRef = React.useRef(null); const { color } = useToastContext(); // Handle expanded state const isExpandedState = controlledExpanded !== undefined ? controlledExpanded : internalExpanded; // Determine actual status (for promise-based toasts) const actualStatus = status || (type === 'success' ? 'success' : type === 'error' ? 'error' : 'idle'); // Improved progress bar with pause on hover and better accuracy useEffect(() => { if (typeof progress !== 'undefined') { setLocalProgress(progress); return; } // If duration is 0, don't auto-dismiss (for loading/promise toasts) if (duration === 0) { setLocalProgress(100); // Keep at 100% (no progress bar animation) return; } // Initialize start time on first mount if (isInitialMountRef.current) { startTimeRef.current = Date.now(); isInitialMountRef.current = false; } // Adjust start time when resuming from pause if (!isPaused && elapsedWhenPausedRef.current > 0) { startTimeRef.current = Date.now() - elapsedWhenPausedRef.current; } const updateProgress = () => { if (isPaused) { // Store elapsed time when paused elapsedWhenPausedRef.current = Date.now() - startTimeRef.current; return; } const elapsed = Date.now() - startTimeRef.current; const progressValue = Math.max(0, Math.min(100, 100 - (elapsed / duration) * 100)); setLocalProgress(progressValue); // Remove toast when progress is very close to 0 (account for animation lag) // This ensures progress bar completes before toast disappears if (progressValue <= 1) { setLocalProgress(0); // Ensure progress is exactly 0 // Clear any pending timeout if (removeTimeoutRef.current !== null) { clearTimeout(removeTimeoutRef.current); } // Very small delay to let progress bar complete its animation removeTimeoutRef.current = setTimeout(() => { removeToast(id); }, 50); } else { animationFrameRef.current = requestAnimationFrame(updateProgress); } }; if (!isPaused && duration > 0) { animationFrameRef.current = requestAnimationFrame(updateProgress); } return () => { if (animationFrameRef.current !== null) { cancelAnimationFrame(animationFrameRef.current); } if (removeTimeoutRef.current !== null) { clearTimeout(removeTimeoutRef.current); } }; }, [duration, progress, isPaused, id, removeToast]); const getIcon = () => { // Show loading spinner for loading status (priority over custom icon) if (actualStatus === 'loading' || status === 'loading') { return (