'use client'; // Lazy peaks fetch with module-cache + in-flight dedupe. Triggers on first // `play()`, IntersectionObserver visibility, or explicit `decodeOnMount` flag. import { useEffect, useState } from 'react'; import { getPeaks, getPeaksFromCache } from '../audio/peaksCache'; export type UsePeaksOptions = { src: string; enabled?: boolean; triggerRef?: React.RefObject; decodeOnMount?: boolean; }; export function usePeaks(opts: UsePeaksOptions): { peaks: Float32Array | null; loading: boolean; error: unknown; } { const { src, enabled = true, triggerRef, decodeOnMount = false } = opts; const cached = getPeaksFromCache(src) ?? null; const [peaks, setLocal] = useState(cached); const [loading, setLoading] = useState(!cached && enabled); const [error, setError] = useState(null); useEffect(() => { if (!enabled) return; const hit = getPeaksFromCache(src); if (hit) { setLocal(hit); setLoading(false); return; } setLocal(null); setLoading(true); let cancelled = false; let started = false; const startDecode = () => { if (started) return; started = true; getPeaks(src) .then((p) => { if (cancelled) return; setLocal(p); setLoading(false); }) .catch((e) => { if (cancelled) return; setError(e); setLoading(false); }); }; if (decodeOnMount || !triggerRef?.current || typeof IntersectionObserver === 'undefined') { startDecode(); return () => { cancelled = true; }; } const obs = new IntersectionObserver( (entries) => { for (const entry of entries) { if (entry.isIntersecting) { startDecode(); obs.disconnect(); break; } } }, { rootMargin: '200px' }, ); obs.observe(triggerRef.current); return () => { cancelled = true; obs.disconnect(); }; }, [src, enabled, decodeOnMount, triggerRef]); return { peaks, loading, error }; }