// Sparkles — restrained particle field for hero / opener backdrops. // // Design goals: // - Professional, NOT playful. No rainbow, no big glittery stars, // no chaotic motion. Tiny brand-tinted dots that fade in and out // out of phase — reads as "atmosphere", not "Christmas tree". // - SSR-safe: positions / sizes / delays derive deterministically // from the index, so server-rendered HTML matches the first // client render (no hydration mismatch, no re-shuffle on each // render). // - Pointer-events off, aria-hidden — purely decorative. // - Respects prefers-reduced-motion: the `motion-safe:` variant on // the twinkle animation means each dot freezes at opacity 0 when // the user has reduced motion enabled. // // Drop this absolute-fill inside any container (hero, section, card) // and it covers the parent. Tune via `count`, `tone`, `intensity`. import type { ReactNode } from 'react' import { cn } from '../cn' interface SparklesProps { /** How many dots to render. Default 22. Sweet spot is 16–28 — * fewer feels accidental, more starts to feel busy. */ readonly count?: number /** Brand mode. Cool = violet/cyan accents (default); warm = amber/rose. */ readonly tone?: 'cool' | 'warm' /** Density tier. `'subtle'` is the default and what most heros want; * `'normal'` bumps the max-opacity peak a bit; `'strong'` makes * the field more obvious. Each tier still respects the * "professional, not playful" budget. */ readonly intensity?: 'subtle' | 'normal' | 'strong' readonly className?: string } // Two prime-ish multipliers per axis. Spreads positions evenly across // the container without lining up on a grid. Picked by eye — change // only if you want a different distribution. const FREQ_X = 137.5 const FREQ_Y = 73.13 const INTENSITY_OPACITY: Record, string> = { subtle: 'opacity-50', normal: 'opacity-75', strong: 'opacity-100', } export const Sparkles = ({ count = 22, tone = 'cool', intensity = 'subtle', className, }: SparklesProps): ReactNode => { // Dot CORE is near-white so it pops against the brand-tinted // MeshBackdrop blobs (those use the same violet/cyan and would // swallow same-coloured dots). The HALO is brand-tinted — that's // what visually anchors the sparkle to the rest of the palette // without coloring the dot itself. const haloColors = tone === 'warm' ? ['oklch(0.78 0.16 60)', 'oklch(0.72 0.18 25)'] : ['oklch(0.78 0.18 290)', 'oklch(0.74 0.16 220)'] return ( ) }