// Pointer interaction for the waveform: click-to-seek, drag-to-scrub, // hover indicator + tooltip with time-at-cursor. CSS variables only — no // React state, no canvas redraws. import { formatTime } from '../../utils/formatTime'; const HOVER_X = '--hp'; const HOVER_OPACITY = '--ho'; const TOOLTIP_LABEL = '--ht'; export type AttachSeekOptions = { // When the user clicks while paused/idle, also start playback. Drag scrub // never auto-starts (already playing or user is scrubbing without intent). startsPlayback?: boolean; onPlayRequest?: () => void; }; export function attachSeek( container: HTMLElement, audio: HTMLAudioElement, options: AttachSeekOptions = {}, ): () => void { let dragging = false; let movedDuringDrag = false; const { startsPlayback = true, onPlayRequest } = options; const ratioFor = (clientX: number): number => { const rect = container.getBoundingClientRect(); if (rect.width === 0) return 0; return Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)); }; const seekTo = (clientX: number) => { const dur = Number.isFinite(audio.duration) ? audio.duration : 0; if (dur > 0) audio.currentTime = ratioFor(clientX) * dur; }; const onPointerDown = (e: PointerEvent) => { if (e.button !== 0 && e.pointerType === 'mouse') return; dragging = true; movedDuringDrag = false; container.setPointerCapture?.(e.pointerId); seekTo(e.clientX); }; const onPointerMove = (e: PointerEvent) => { if (!dragging) return; movedDuringDrag = true; seekTo(e.clientX); }; const onPointerEnd = (e: PointerEvent) => { if (!dragging) return; const wasDrag = movedDuringDrag; dragging = false; movedDuringDrag = false; try { container.releasePointerCapture?.(e.pointerId); } catch { // ignore } // Click (not drag) while paused → start playback at the seek target. if (!wasDrag && startsPlayback && (audio.paused || audio.ended)) { onPlayRequest?.(); } }; container.addEventListener('pointerdown', onPointerDown); container.addEventListener('pointermove', onPointerMove); container.addEventListener('pointerup', onPointerEnd); container.addEventListener('pointercancel', onPointerEnd); return () => { container.removeEventListener('pointerdown', onPointerDown); container.removeEventListener('pointermove', onPointerMove); container.removeEventListener('pointerup', onPointerEnd); container.removeEventListener('pointercancel', onPointerEnd); }; } export function attachHover(container: HTMLElement, audio: HTMLAudioElement): () => void { const tooltip = container.querySelector('[data-audioplayer-time-tip]'); const onMove = (e: PointerEvent) => { const rect = container.getBoundingClientRect(); if (rect.width === 0) return; const x = Math.max(0, Math.min(rect.width, e.clientX - rect.left)); container.style.setProperty(HOVER_X, `${x}px`); container.style.setProperty(HOVER_OPACITY, '1'); if (tooltip) { const dur = Number.isFinite(audio.duration) ? audio.duration : 0; const t = (x / rect.width) * dur; tooltip.textContent = formatTime(t); tooltip.style.setProperty(TOOLTIP_LABEL, '1'); } }; const onLeave = () => { container.style.setProperty(HOVER_OPACITY, '0'); if (tooltip) tooltip.style.setProperty(TOOLTIP_LABEL, '0'); }; container.addEventListener('pointermove', onMove); container.addEventListener('pointerleave', onLeave); return () => { container.removeEventListener('pointermove', onMove); container.removeEventListener('pointerleave', onLeave); }; }