# `karaoke/use-lyrics` _(no submodules)_ ## Purpose `useLyrics(opts)` is the engine-agnostic karaoke hook. It owns the active-line state, samples it at a fixed interval (100 ms by default), and returns `{ lyrics, activeIndex, activeLyric }`. The consumer hands in either pre-parsed `Lyric[]` or raw LRC source, plus the current playback time in seconds. The hook deliberately does not call `useTransportTime` itself so it can be driven from any clock — `` wires the transport in for you, but a preview panel running under a deterministic test clock can call this hook directly. The 100 ms re-bind ceiling matches the spec requirement that the active lyric advance within 100 ms of the transport. Sampling at that cadence (rather than per-frame) saves a substantial amount of React commit work on songs with hundreds of lyric lines. ## Features - Pure: no audio context, no global side-effect. - Pre-parsed `lyrics` or raw `lrc` source — both accepted; if both provided, `lyrics` wins (no double work). - Configurable `intervalMs` (default 100). The hook re-evaluates the active line on each tick and only commits when the index actually changes — silent renders cost nothing. - Returns `activeLyric: undefined` before the first cue or when `lyrics` is empty. ## Usage: Basic ```tsx import { useLyrics } from "@suaveplan/react-audio-player/karaoke/use-lyrics"; import { useTransportTime } from "@suaveplan/react-audio"; function Lyrics({ lrc }: { lrc: string }) { const seconds = useTransportTime(); const { activeLyric } = useLyrics({ lrc, currentSec: seconds }); return {activeLyric?.text ?? "…"}; } ``` ## Usage: Advanced ```tsx import { parseLrc, useLyrics, } from "@suaveplan/react-audio-player/karaoke"; import { useMemo, useState } from "react"; function StandaloneLyricPreview({ lrc }: { lrc: string }) { // Parse once at the call site rather than on every mount. const lyrics = useMemo(() => parseLrc(lrc), [lrc]); // Deterministic clock — no required. const [tick, setTick] = useState(0); const { activeIndex, activeLyric, lyrics: ls } = useLyrics({ lyrics, currentSec: tick * 0.5, intervalMs: 50, }); return (
    {ls.map((l, i) => (
  1. {l.text}
  2. ))}

Active: {activeLyric?.text ?? "—"}

); } ``` ## API Reference - `useLyrics(opts: UseLyricsOptions): UseLyricsResult` - `UseLyricsOptions`: - `lyrics?: readonly Lyric[]` — preferred. - `lrc?: string` — parsed once per identity change. - `intervalMs?: number` (default 100). - `currentSec: number` — required. - `UseLyricsResult`: - `lyrics: readonly Lyric[]`, - `activeIndex: number` (`-1` for "before first cue / empty"), - `activeLyric: Lyric | undefined`. ## Implementation Notes The hook owns one piece of React state — the active index — and one `setInterval`. Both `lyrics` and `currentSec` are mirrored through refs so the interval callback always sees the latest values without re-binding the timer on every render. The interval only fires `setActiveIndex` when the recomputed index actually differs, which is the standard React micro-optimisation for high-frequency external clocks. `activeLyricIndex` (from the sibling `lrc-parser` module) is a binary search; combined with the 100 ms sampling cadence, the hook's steady-state work per frame is one comparison plus a "same index?" branch. Long-form lyrics (10k+ lines) are not measurably more expensive than short ones. The hook lazily initialises its `activeIndex` state from `activeLyricIndex(lyrics, currentSec)` so the first render — before the effect mounts — already reflects the correct active line. This matters for SSR-rendered previews and snapshot tests that read the hook's output without ever ticking the interval.