'use client'; import { forwardRef, HTMLAttributes, useEffect, useState, useRef } from 'react'; export interface CountdownDisplayProps extends Omit, 'children'> { target: Date | number; format?: 'full' | 'hms' | 'ms' | 'dhms'; size?: 'sm' | 'md' | 'lg'; variant?: 'default' | 'minimal' | 'neon' | 'brutal' | 'glitch'; accentColor?: string; showLabels?: boolean; compact?: boolean; flip?: boolean; urgentThreshold?: number; onComplete?: () => void; labels?: { days?: string; hours?: string; minutes?: string; seconds?: string; }; } interface TimeLeft { days: number; hours: number; minutes: number; seconds: number; } const sizeClasses = { sm: { value: 'text-3xl md:text-5xl', separator: 'text-xl md:text-3xl', label: 'text-[0.5rem]' }, md: { value: 'text-5xl md:text-7xl', separator: 'text-3xl md:text-5xl', label: 'text-[0.6rem]' }, lg: { value: 'text-7xl md:text-9xl', separator: 'text-5xl md:text-7xl', label: 'text-[0.7rem]' }, }; export const CountdownDisplay = forwardRef( ( { target, format = 'hms', size = 'md', variant = 'default', accentColor = '#ff0040', showLabels = true, compact = false, flip = false, urgentThreshold = 60, onComplete, labels = {}, className = '', ...props }, ref ) => { const [timeLeft, setTimeLeft] = useState({ days: 0, hours: 0, minutes: 0, seconds: 0 }); const [isUrgent, setIsUrgent] = useState(false); const completedRef = useRef(false); const defaultLabels = { days: labels.days || 'DAYS', hours: labels.hours || 'HOURS', minutes: labels.minutes || 'MIN', seconds: labels.seconds || 'SEC', }; useEffect(() => { const calculateTimeLeft = (): TimeLeft => { let totalSeconds: number; if (target instanceof Date) { totalSeconds = Math.max(0, Math.floor((target.getTime() - Date.now()) / 1000)); } else { totalSeconds = Math.max(0, target); } return { days: Math.floor(totalSeconds / 86400), hours: Math.floor((totalSeconds % 86400) / 3600), minutes: Math.floor((totalSeconds % 3600) / 60), seconds: totalSeconds % 60, }; }; const tick = () => { const newTime = calculateTimeLeft(); const totalSeconds = newTime.days * 86400 + newTime.hours * 3600 + newTime.minutes * 60 + newTime.seconds; setTimeLeft(newTime); setIsUrgent(totalSeconds <= urgentThreshold && totalSeconds > 0); if (totalSeconds === 0 && !completedRef.current) { completedRef.current = true; onComplete?.(); } }; tick(); const interval = setInterval(tick, 1000); return () => clearInterval(interval); }, [target, urgentThreshold, onComplete]); const pad = (num: number) => String(num).padStart(2, '0'); const { value: valueClass, separator: sepClass, label: labelClass } = sizeClasses[size]; const getValueStyle = () => { switch (variant) { case 'neon': return { color: accentColor, textShadow: `0 0 10px ${accentColor}, 0 0 20px ${accentColor}, 0 0 40px ${accentColor}` }; case 'brutal': return { background: accentColor, color: '#0a0a0a', padding: '0 0.25em', WebkitTextFillColor: '#0a0a0a' }; case 'minimal': return { color: '#fafafa' }; default: return { background: 'linear-gradient(180deg, #fafafa 0%, #888 100%)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }; } }; const renderBlock = (value: number, label: string) => (
{pad(value)} {showLabels && ( {label} )}
); const renderSeparator = (key: number) => ( : ); const blocks = []; if (format === 'dhms' || format === 'full') { blocks.push(
{renderBlock(timeLeft.days, defaultLabels.days)}
); blocks.push(renderSeparator(1)); } if (format !== 'ms') { blocks.push(
{renderBlock(timeLeft.hours, defaultLabels.hours)}
); blocks.push(renderSeparator(2)); } blocks.push(
{renderBlock(timeLeft.minutes, defaultLabels.minutes)}
); blocks.push(renderSeparator(3)); blocks.push(
{renderBlock(timeLeft.seconds, defaultLabels.seconds)}
); return (
{blocks}
); } ); CountdownDisplay.displayName = 'CountdownDisplay'; export default CountdownDisplay;