import { useRef, useEffect, useState, useCallback } from 'react' interface Clip { id: string title?: string prompt: string duration_seconds: number } interface Props { clip: Clip isPlaying: boolean onTogglePlay: () => void onStop: () => void onClose: () => void } function formatTime(seconds: number): string { const mins = Math.floor(seconds / 60) const secs = Math.floor(seconds % 60) return `${mins}:${secs.toString().padStart(2, '0')}` } export default function Player({ clip, isPlaying, onTogglePlay, onStop, onClose }: Props) { const audioRef = useRef(null) const [currentTime, setCurrentTime] = useState(0) const [duration, setDuration] = useState(clip.duration_seconds) const [volume, setVolume] = useState(1) const [isMuted, setIsMuted] = useState(false) const [isDragging, setIsDragging] = useState(false) const [isRepeating, setIsRepeating] = useState(false) const progressRef = useRef(null) // Sync play/pause with audio element useEffect(() => { if (audioRef.current) { if (isPlaying) { audioRef.current.play().catch(() => { // Autoplay blocked or other error }) } else { audioRef.current.pause() } } }, [isPlaying, clip.id]) // Update duration when metadata loads const handleLoadedMetadata = () => { if (audioRef.current) { setDuration(audioRef.current.duration || clip.duration_seconds) } } // Update current time during playback const handleTimeUpdate = () => { if (audioRef.current && !isDragging) { setCurrentTime(audioRef.current.currentTime) } } // Track ended const handleEnded = () => { if (!isRepeating) { onTogglePlay() } setCurrentTime(0) } // Toggle repeat const toggleRepeat = () => { setIsRepeating(prev => { const next = !prev if (audioRef.current) { audioRef.current.loop = next } return next }) } // Seek to position const handleSeek = useCallback((clientX: number) => { if (!progressRef.current || !audioRef.current) return const rect = progressRef.current.getBoundingClientRect() const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)) const newTime = ratio * duration audioRef.current.currentTime = newTime setCurrentTime(newTime) }, [duration]) // Progress bar mouse events const handleProgressMouseDown = (e: React.MouseEvent) => { setIsDragging(true) handleSeek(e.clientX) } const handleProgressMouseMove = (e: React.MouseEvent) => { if (isDragging) { handleSeek(e.clientX) } } const handleProgressMouseUp = () => { setIsDragging(false) } // Global mouse events for drag useEffect(() => { if (isDragging) { const handleMouseMove = (e: MouseEvent) => handleSeek(e.clientX) const handleMouseUp = () => setIsDragging(false) window.addEventListener('mousemove', handleMouseMove) window.addEventListener('mouseup', handleMouseUp) return () => { window.removeEventListener('mousemove', handleMouseMove) window.removeEventListener('mouseup', handleMouseUp) } } }, [isDragging, handleSeek]) // Volume control const handleVolumeChange = (e: React.ChangeEvent) => { const newVol = parseFloat(e.target.value) setVolume(newVol) if (audioRef.current) { audioRef.current.volume = newVol } if (newVol > 0 && isMuted) { setIsMuted(false) } } const toggleMute = () => { if (!audioRef.current) return if (isMuted) { audioRef.current.volume = volume > 0 ? volume : 0.5 setIsMuted(false) } else { audioRef.current.volume = 0 setIsMuted(true) } } // Restart from beginning const handleRestart = () => { if (audioRef.current) { audioRef.current.currentTime = 0 setCurrentTime(0) if (!isPlaying) { onTogglePlay() } } } // Stop const handleStop = () => { if (audioRef.current) { audioRef.current.pause() audioRef.current.currentTime = 0 setCurrentTime(0) } onStop() } const progress = duration > 0 ? (currentTime / duration) * 100 : 0 const displayTitle = clip.title || clip.prompt const marqueeDuration = Math.max(3, displayTitle.length * 0.15) return ( <>
{/* Play/Pause */} {/* Stop */} {/* Restart — ISO skip backwards to beginning */} {/* Repeat — ISO repeat indefinitely with glowing toggle */} {/* Time + Scrubber */}
{formatTime(currentTime)}
{/* Scrubber handle */}
{formatTime(duration)}
{/* Track info with artwork */}
{ // Fallback to treble clef on error const target = e.target as HTMLImageElement target.style.display = 'none' const parent = target.parentElement if (parent) { parent.innerHTML = `𝄞` } }} />
{/* Marquee scrolling track name */}
{displayTitle} {displayTitle}
{/* Volume */}
{/* Close */}
) }