import { useEffect, useMemo, useState } from "react"; import { activeLyricIndex, parseLrc } from "../lrc-parser/lrc-parser.js"; import type { Lyric } from "../lrc-parser/lrc-parser.types.js"; import type { UseLyricsOptions, UseLyricsResult } from "./use-lyrics.types.js"; const DEFAULT_INTERVAL_MS = 100; /** * Engine-agnostic karaoke hook. Accepts a pre-parsed `lyrics` array * (preferred) or a raw `lrc` string, plus a `currentSec` value, and * returns the active line. Sampling cadence is decoupled from the * caller's render rate: a 100 ms interval (configurable) forces a * re-render even when neither `lyrics` nor `currentSec` mutate. * * The hook is deliberately decoupled from `useTransportTime` so it * can be driven from any clock — `` wires the * transport in for you when used inside ``, but a * preview panel running under a deterministic test clock can call * this hook directly with a hand-fed `currentSec`. */ export function useLyrics(opts: UseLyricsOptions): UseLyricsResult { const lyrics: readonly Lyric[] = useMemo(() => { if (opts.lyrics) return opts.lyrics; if (typeof opts.lrc === "string") return parseLrc(opts.lrc); return []; }, [opts.lyrics, opts.lrc]); const intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS; // A heartbeat counter forces a re-render every `intervalMs` so // the memo below recomputes even when `currentSec` did not // change between commits — relevant when the consumer's clock // mutates a ref or external store the hook cannot subscribe to. const [, setTick] = useState(0); useEffect(() => { const id = setInterval( () => { setTick((t) => (t + 1) % 1_000_000); }, Math.max(1, intervalMs) ); return () => { clearInterval(id); }; }, [intervalMs]); const activeIndex = useMemo( () => activeLyricIndex(lyrics, opts.currentSec), [lyrics, opts.currentSec] ); return { lyrics, activeIndex, activeLyric: activeIndex >= 0 ? lyrics[activeIndex] : undefined, }; }