import * as stylex from "@stylexjs/stylex";
import { type PropsWithChildren, memo, useState } from "react";
import usePrefersReducedMotion from "./usePrefersReducedMotion";
import useRandomInterval from "./useRandomInterval";
// Taken and adapted from:
// https://www.joshwcomeau.com/react/animated-sparkles-in-react/
const spin = stylex.keyframes({
"0%": { transform: "rotate(0deg)" },
"100%": { transform: "rotate(180deg)" },
});
const comeInOut = stylex.keyframes({
"0%": { transform: "scale(0)" },
"50%": { transform: "scale(1)" },
"100%": { transform: "scale(0)" },
});
const noPreference = "@media (prefers-reduced-motion: no-preference)";
const styles = stylex.create({
wrapper: {
display: "inline-block",
position: "relative",
},
childWrapper: {
position: "relative",
zIndex: 1,
fontWeight: 600,
},
sparkleSvg: {
display: "block",
animationName: {
[noPreference]: spin,
},
animationTimingFunction: {
[noPreference]: "linear",
},
animationDuration: {
[noPreference]: "1000ms",
},
},
sparkleWrapper: {
display: "block",
position: "absolute",
animationName: {
[noPreference]: comeInOut,
},
animationDuration: {
[noPreference]: "850ms",
},
animationFillMode: {
[noPreference]: "forwards",
},
},
});
const DEFAULT_COLOR = "var(--t-subscription-color)";
const random = (min: number, max: number) =>
((Math.random() * (max - min)) | 0) + min;
const generateSpark = (color: string) => ({
id: String(random(10000, 99999)),
createdAt: Date.now(),
color,
size: random(10, 20),
style: {
top: `${random(-50, 100)}%`,
left: `${random(-10, 100)}%`,
},
});
export interface SparklesProps extends PropsWithChildren {
color?: string;
// Support for delegated props were removed. Maybe we'll add them later.
// ...delegated
}
export default memo(function Sparkles(props: SparklesProps) {
const color = props.color ?? DEFAULT_COLOR;
const [sparkles, setSparkles] = useState(() => {
return [
generateSpark(color),
generateSpark(color),
generateSpark(color),
];
});
const prefersReducedMotion = usePrefersReducedMotion();
useRandomInterval(
() => {
const sparkle = generateSpark(color);
const now = Date.now();
const nextSparkles = sparkles.filter(
(sp) => now - sp.createdAt < 1000,
);
nextSparkles.push(sparkle);
setSparkles(nextSparkles);
},
prefersReducedMotion ? null : 450,
prefersReducedMotion ? null : 800,
);
return (
{sparkles.map((sparkle) => (
))}
{props.children}
);
});
interface SparkleProps {
size: number;
color: string;
style: stylex.StaticStyles<{ top: number | string; left: number | string }>;
}
const Sparkle = memo(function Sparkle({ size, color, style }: SparkleProps) {
const path =
"M26.5 25.5C19.0043 33.3697 0 34 0 34C0 34 19.1013 35.3684 26.5 43.5C33.234 50.901 34 68 34 68C34 68 36.9884 50.7065 44.5 43.5C51.6431 36.647 68 34 68 34C68 34 51.6947 32.0939 44.5 25.5C36.5605 18.2235 34 0 34 0C34 0 33.6591 17.9837 26.5 25.5Z";
return (
{/* biome-ignore lint/a11y/noSvgWithoutTitle: It's a spark. */}
);
});