86 lines
2.5 KiB
TypeScript
86 lines
2.5 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* Confetti — a lightweight, dependency-free confetti shower for the reward
|
|
* celebration. Spawns a fixed set of colored pieces that fall with a little
|
|
* horizontal drift and spin, then fade. Pointer-events-none so it never
|
|
* blocks the dialog beneath it. Honors prefers-reduced-motion.
|
|
*/
|
|
|
|
import { useMemo } from "react";
|
|
|
|
const COLORS = [
|
|
"#f59e0b",
|
|
"#fb7185",
|
|
"#34d399",
|
|
"#60a5fa",
|
|
"#fbbf24",
|
|
"#f472b6",
|
|
"#a78bfa",
|
|
"#facc15",
|
|
];
|
|
|
|
// Deterministic pseudo-random in [0,1) from a seed. Math.sin is pure, so this
|
|
// keeps the confetti layout stable across renders (no Math.random impurity).
|
|
function rand(seed: number): number {
|
|
const x = Math.sin(seed * 127.1) * 43758.5453;
|
|
return x - Math.floor(x);
|
|
}
|
|
|
|
export function Confetti({ pieces = 140 }: { pieces?: number }) {
|
|
// Computed once per mount; varying by index keeps it deterministic-ish
|
|
// without Date/Math.random concerns (this is a normal component).
|
|
const items = useMemo(
|
|
() =>
|
|
Array.from({ length: pieces }, (_, i) => {
|
|
const r = (k: number) => rand(i * 9.17 + k * 3.13);
|
|
return {
|
|
left: r(1) * 100,
|
|
delay: r(2) * 0.5,
|
|
dur: 2.4 + r(3) * 1.8,
|
|
rot: r(4) * 360,
|
|
drift: (r(5) * 2 - 1) * 70,
|
|
size: 6 + r(6) * 7,
|
|
color: COLORS[i % COLORS.length],
|
|
round: r(7) < 0.3,
|
|
};
|
|
}),
|
|
[pieces],
|
|
);
|
|
|
|
return (
|
|
<div
|
|
className="pointer-events-none absolute inset-0 overflow-hidden"
|
|
aria-hidden
|
|
>
|
|
<style>{`
|
|
@keyframes sadhna-confetti {
|
|
0% { transform: translate(0,-10%) rotateZ(0deg); opacity: 1; }
|
|
85% { opacity: 1; }
|
|
100% { transform: translate(var(--drift), 115vh) rotateZ(720deg); opacity: 0; }
|
|
}
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.sadhna-confetti-piece { display: none; }
|
|
}
|
|
`}</style>
|
|
{items.map((p, i) => (
|
|
<span
|
|
key={i}
|
|
className="sadhna-confetti-piece absolute top-0 block"
|
|
style={{
|
|
left: `${p.left}%`,
|
|
width: p.size,
|
|
height: p.size * (p.round ? 1 : 0.5),
|
|
background: p.color,
|
|
borderRadius: p.round ? "9999px" : "1px",
|
|
transform: `rotateZ(${p.rot}deg)`,
|
|
// CSS var consumed by the keyframe's final transform.
|
|
["--drift" as string]: `${p.drift}px`,
|
|
animation: `sadhna-confetti ${p.dur}s cubic-bezier(0.2,0.6,0.4,1) ${p.delay}s forwards`,
|
|
}}
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|