/** * Playback position, published outside React. * * The lyrics pane no longer lives in the player's subtree — it renders in a panel of the /music * workspace — so the position has to cross the tree. It cannot cross as state: the engine reports a new * position every animation frame, and a shared state channel would re-render every consumer 60× a * second. Instead the host pushes into this module and subscribers decide for themselves what is worth * a render (the lyrics pane only re-renders when the ACTIVE LINE changes, roughly once a line). * * Seek travels the other way for the same reason: the engine is the host's, but a click on a lyric line * has to reach it. */ let position = 0; let duration = 0; const subscribers = new Set<(sec: number, dur: number) => void>(); let seekFn: ((sec: number) => void) | null = null; export const publishPlayerTime = (sec: number, dur: number): void => { position = sec; duration = dur; for (const fn of subscribers) fn(sec, dur); }; /** Latest values, for a subscriber that mounts mid-track. */ export const getPlayerTime = (): number => position; export const getPlayerDuration = (): number => duration; export const subscribePlayerTime = (fn: (sec: number, dur: number) => void): (() => void) => { subscribers.add(fn); return () => { subscribers.delete(fn); }; }; export const registerPlayerSeek = (fn: (sec: number) => void): (() => void) => { seekFn = fn; return () => { if (seekFn === fn) seekFn = null; }; }; export const seekPlayer = (sec: number): void => seekFn?.(sec);