// AVA — the green beat indicator. // // A circular component: emerald center dot + thin SVG phase ring that fills // with beat.phase over each beat period. On every beat pulse the dot scales up // and glows (~180 ms). When the pulse came from the *predictor* the flash is // rendered as an expanding outline ring + "PRED" badge instead of a filled // dot, so prediction is visible at a glance. 'use client' import { useEffect, useState } from 'react' import { motion } from 'framer-motion' import { useAvaStore } from '@/lib/store' import { cn } from '@/lib/utils' interface BeatDotProps { size?: number className?: string } export function BeatDot({ size = 44, className }: BeatDotProps) { const phase = useAvaStore((s) => s.stats?.beat.phase ?? 0) const beatFlash = useAvaStore((s) => s.beatFlash) const bpm = useAvaStore((s) => s.stats?.beat.bpm ?? 0) const idle = useAvaStore((s) => s.source) === 'idle' // flash state is derived: a flash is "on" until its seq is cleared by the // hold timeout (setState only ever happens inside the timeout callback) const [clearedSeq, setClearedSeq] = useState(-1) useEffect(() => { if (!beatFlash) return const hold = beatFlash.source === 'predicted' ? 450 : 180 const t = setTimeout(() => setClearedSeq(beatFlash.seq), hold) return () => clearTimeout(t) }, [beatFlash]) const flashing = beatFlash !== null && beatFlash.seq > clearedSeq const flashSource = flashing ? beatFlash.source : 'none' const r = size / 2 - 3 const c = 2 * Math.PI * r const predicted = flashing && flashSource === 'predicted' const dotBase = Math.max(8, size * 0.3) return (