'use client'; // One rAF loop reads audio.currentTime and writes a single CSS variable. // Pauses on document.hidden and when the audio is paused (with a final // flush to keep the bar in sync after the seek that triggered the pause). import { useEffect } from 'react'; const VAR = '--p'; export function usePlayheadLoop( audio: HTMLAudioElement, el: HTMLElement | null, enabled = true, ): void { useEffect(() => { if (!enabled || !el) return; let raf = 0; let lastPct = -1; const writePct = () => { const dur = audio.duration; if (!Number.isFinite(dur) || dur <= 0) return; const pct = Math.max(0, Math.min(100, (audio.currentTime / dur) * 100)); if (Math.abs(pct - lastPct) < 0.01) return; lastPct = pct; el.style.setProperty(VAR, `${pct.toFixed(2)}%`); }; const tick = () => { if (typeof document !== 'undefined' && document.hidden) { raf = 0; return; } writePct(); raf = requestAnimationFrame(tick); }; const start = () => { if (raf) return; raf = requestAnimationFrame(tick); }; const stop = () => { if (!raf) return; cancelAnimationFrame(raf); raf = 0; writePct(); }; if (!audio.paused) start(); const onPlay = () => start(); const onPauseOrEnd = () => stop(); const onSeek = () => writePct(); const onVisibility = () => { if (document.hidden) stop(); else if (!audio.paused) start(); }; audio.addEventListener('play', onPlay); audio.addEventListener('pause', onPauseOrEnd); audio.addEventListener('ended', onPauseOrEnd); audio.addEventListener('seeked', onSeek); audio.addEventListener('timeupdate', writePct); document.addEventListener('visibilitychange', onVisibility); return () => { stop(); audio.removeEventListener('play', onPlay); audio.removeEventListener('pause', onPauseOrEnd); audio.removeEventListener('ended', onPauseOrEnd); audio.removeEventListener('seeked', onSeek); audio.removeEventListener('timeupdate', writePct); document.removeEventListener('visibilitychange', onVisibility); }; }, [audio, el, enabled]); }