'use client'; import { useCallback, useEffect, useRef, useState } from 'react'; import { Loader2, Pause, Play } from 'lucide-react'; import { cn } from '@/lib/utils'; const SIZE = 240; const RING_WIDTH = 4; const RADIUS = (SIZE - RING_WIDTH) / 2; const CIRCUMFERENCE = 2 * Math.PI * RADIUS; function formatDuration(seconds: number) { if (!Number.isFinite(seconds) || seconds <= 0) return '0:00'; const whole = Math.floor(seconds); const mins = Math.floor(whole / 60); const secs = whole % 60; return `${mins}:${secs.toString().padStart(2, '0')}`; } function progressFromPointer(clientX: number, clientY: number, rect: DOMRect) { const cx = rect.left + rect.width / 2; const cy = rect.top + rect.height / 2; const dx = clientX - cx; const dy = clientY - cy; let angle = Math.atan2(dy, dx) + Math.PI / 2; if (angle < 0) angle += Math.PI * 2; return Math.min(Math.max(angle / (Math.PI * 2), 0), 1); } export function ChatVideoNotePlayer({ src, durationMs, variant = 'theirs' }: { src: string; durationMs?: number; variant?: 'mine' | 'theirs'; }) { const rootRef = useRef(null); const videoRef = useRef(null); const scrubbingRef = useRef(false); const wasPlayingRef = useRef(false); const [playing, setPlaying] = useState(false); const [loading, setLoading] = useState(true); const [scrubbing, setScrubbing] = useState(false); const [progress, setProgress] = useState(0); const [duration, setDuration] = useState(durationMs ? durationMs / 1000 : 0); const ringColor = variant === 'mine' ? '#7ea06a' : '#3390ec'; const seekTo = useCallback((ratio: number) => { const video = videoRef.current; if (!video || !Number.isFinite(video.duration) || video.duration <= 0) return; const next = Math.min(Math.max(ratio, 0), 1); video.currentTime = next * video.duration; setProgress(next); }, []); useEffect(() => { const video = videoRef.current; if (!video) return; const onLoaded = () => { if (Number.isFinite(video.duration) && video.duration > 0) { setDuration(video.duration); } setLoading(false); }; const onTimeUpdate = () => { if (scrubbingRef.current || !video.duration) return; setProgress(video.currentTime / video.duration); }; const onPlay = () => setPlaying(true); const onPause = () => setPlaying(false); const onEnded = () => { setPlaying(false); setProgress(0); video.currentTime = 0; }; video.addEventListener('loadedmetadata', onLoaded); video.addEventListener('timeupdate', onTimeUpdate); video.addEventListener('play', onPlay); video.addEventListener('pause', onPause); video.addEventListener('ended', onEnded); return () => { video.removeEventListener('loadedmetadata', onLoaded); video.removeEventListener('timeupdate', onTimeUpdate); video.removeEventListener('play', onPlay); video.removeEventListener('pause', onPause); video.removeEventListener('ended', onEnded); }; }, [src]); useEffect(() => { function handlePointerMove(event: PointerEvent) { if (!scrubbingRef.current || !rootRef.current) return; const rect = rootRef.current.getBoundingClientRect(); seekTo(progressFromPointer(event.clientX, event.clientY, rect)); } function handlePointerUp() { if (!scrubbingRef.current) return; scrubbingRef.current = false; setScrubbing(false); const video = videoRef.current; if (video && wasPlayingRef.current) { void video.play(); } } window.addEventListener('pointermove', handlePointerMove); window.addEventListener('pointerup', handlePointerUp); window.addEventListener('pointercancel', handlePointerUp); return () => { window.removeEventListener('pointermove', handlePointerMove); window.removeEventListener('pointerup', handlePointerUp); window.removeEventListener('pointercancel', handlePointerUp); }; }, [seekTo]); function togglePlayback() { const video = videoRef.current; if (!video || scrubbingRef.current) return; if (video.paused) { void video.play(); } else { video.pause(); } } function handleScrubStart(event: React.PointerEvent) { if (!rootRef.current) return; event.preventDefault(); event.stopPropagation(); scrubbingRef.current = true; setScrubbing(true); const video = videoRef.current; wasPlayingRef.current = Boolean(video && !video.paused); video?.pause(); const rect = rootRef.current.getBoundingClientRect(); seekTo(progressFromPointer(event.clientX, event.clientY, rect)); } const showControls = !playing || scrubbing || loading; const remaining = duration * (1 - progress); return (
); }