"use client"; import { useEffect, useRef, useCallback } from "react"; interface ConfettiBurstProps { trigger: boolean; particleCount?: number; duration?: number; colors?: string[]; } const DEFAULT_COLORS = [ "#6366f1", // indigo "#8b5cf6", // violet "#ec4899", // pink "#f59e0b", // amber "#10b981", // emerald "#3b82f6", // blue ]; const KEYFRAMES_ID = "confetti-burst-keyframes"; function ensureKeyframes() { if (typeof document === "undefined") return; if (document.getElementById(KEYFRAMES_ID)) return; const style = document.createElement("style"); style.id = KEYFRAMES_ID; style.textContent = ` @keyframes confetti-burst { 0% { transform: translate(0, 0) rotate(0deg) scale(1); opacity: 1; } 100% { transform: translate(var(--confetti-x), var(--confetti-y)) rotate(var(--confetti-r)) scale(0.3); opacity: 0; } } `; document.head.appendChild(style); } export function ConfettiBurst({ trigger, particleCount = 20, duration = 800, colors = DEFAULT_COLORS, }: ConfettiBurstProps) { const containerRef = useRef(null); const prevTrigger = useRef(false); const burst = useCallback(() => { const container = containerRef.current; if (!container) return; ensureKeyframes(); const particles: HTMLDivElement[] = []; for (let i = 0; i < particleCount; i++) { const el = document.createElement("div"); const angle = (Math.PI * 2 * i) / particleCount + (Math.random() - 0.5) * 0.5; const distance = 40 + Math.random() * 80; const x = Math.cos(angle) * distance; const y = Math.sin(angle) * distance - 20 + Math.random() * 60; // gravity bias const rotation = Math.random() * 720 - 360; el.style.cssText = ` position: absolute; left: 50%; top: 50%; width: 6px; height: 6px; margin: -3px 0 0 -3px; border-radius: ${Math.random() > 0.5 ? "50%" : "1px"}; background: ${colors[i % colors.length]}; pointer-events: none; --confetti-x: ${x}px; --confetti-y: ${y}px; --confetti-r: ${rotation}deg; animation: confetti-burst ${duration}ms cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; `; container.appendChild(el); particles.push(el); } setTimeout(() => { particles.forEach((el) => el.remove()); }, duration + 50); }, [particleCount, duration, colors]); useEffect(() => { if (trigger && !prevTrigger.current) { burst(); } prevTrigger.current = trigger; }, [trigger, burst]); return (