import { useEffect, useMemo, useState, useId } from "react"; type ConfettiBurstProps = { className?: string; colors?: string[]; count?: number; }; type Piece = { id: number; style: React.CSSProperties & ConfettiVars; }; type ConfettiVars = { ["--x0"]: string; ["--y0"]: string; ["--x1"]: string; ["--y1"]: string; }; const hashStringToUint32 = (value: string) => { // FNV-1a 32-bit let hash = 2166136261; for (let i = 0; i < value.length; i++) { hash ^= value.charCodeAt(i); hash = Math.imul(hash, 16777619); } return hash >>> 0; }; const mulberry32 = (seed: number) => { return () => { let t = (seed += 0x6d2b79f5); t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; }; export const ConfettiBurst = ({ className, colors = [ "var(--color-primary)", "var(--color-accent)", "var(--color-primary-light)", "var(--color-accent-light)", ], count = 18, }: ConfettiBurstProps) => { const [visible, setVisible] = useState(true); const instanceId = useId(); useEffect(() => { const t = window.setTimeout(() => setVisible(false), 1100); return () => window.clearTimeout(t); }, []); const pieces = useMemo(() => { const seed = hashStringToUint32(`${instanceId}:${count}:${colors.join(",")}`); const rand = mulberry32(seed); const rnd = (min: number, max: number) => min + rand() * (max - min); return Array.from({ length: count }).map((_, i) => { const x0 = rnd(-6, 6); const y0 = rnd(-6, 6); const x1 = rnd(-160, 160); const y1 = rnd(60, 220); const delay = rnd(0, 120); const rotate = rnd(0, 360); const color = colors[i % colors.length]; return { id: i, style: { left: "50%", top: "0%", backgroundColor: color, transform: `translate(${x0}px, ${y0}px) rotate(${rotate}deg)`, animationDelay: `${delay}ms`, // consumed by keyframes ["--x0"]: `${x0}px`, ["--y0"]: `${y0}px`, ["--x1"]: `${x1}px`, ["--y1"]: `${y1}px`, }, }; }); }, [colors, count, instanceId]); if (!visible) return null; return (
{pieces.map((p) => ( ))}
); };