const { useMemo, useState, useEffect, useRef } = React; const ZoomedTimeline = ({ currentTime, faces, isPlaying }) => { // Window configuration (seconds) const WINDOW_SIZE = 20; const HALF_WINDOW = WINDOW_SIZE / 2; // Internal display time for smooth animation (bypasses low-fps currentTime) const [displayTime, setDisplayTime] = useState(currentTime); const lastFrameTime = useRef(performance.now()); const rafRef = useRef(null); // Sync displayTime to currentTime when paused or seeking useEffect(() => { // If the difference is significant (seek), snap immediately if (Math.abs(displayTime - currentTime) > 0.5 || !isPlaying) { setDisplayTime(currentTime); } }, [currentTime, isPlaying]); // RAF Loop for smooth interpolation useEffect(() => { if (isPlaying) { lastFrameTime.current = performance.now(); const loop = () => { const now = performance.now(); const delta = (now - lastFrameTime.current) / 1000; // seconds lastFrameTime.current = now; setDisplayTime(prev => prev + delta); rafRef.current = requestAnimationFrame(loop); }; rafRef.current = requestAnimationFrame(loop); } else { if (rafRef.current) cancelAnimationFrame(rafRef.current); } return () => { if (rafRef.current) cancelAnimationFrame(rafRef.current); }; }, [isPlaying]); const parseTime = (timeStr) => { if (typeof timeStr === 'number') return timeStr; if (!timeStr) return 0; const [h, m, s] = timeStr.split(':'); return parseInt(h) * 3600 + parseInt(m) * 60 + parseFloat(s); }; // Deterministic color generator based on string const getColor = (str) => { const colors = [ 'bg-red-500', 'bg-orange-500', 'bg-amber-500', 'bg-yellow-500', 'bg-lime-500', 'bg-green-500', 'bg-emerald-500', 'bg-teal-500', 'bg-cyan-500', 'bg-sky-500', 'bg-blue-500', 'bg-indigo-500', 'bg-violet-500', 'bg-purple-500', 'bg-fuchsia-500', 'bg-pink-500', 'bg-rose-500' ]; let hash = 0; for (let i = 0; i < str.length; i++) { hash = str.charCodeAt(i) + ((hash << 5) - hash); } const index = Math.abs(hash) % colors.length; return colors[index]; }; // Helper to safely get name (handles object with .value or string) const getName = (face) => { const n = face.name; if (typeof n === 'object' && n !== null) return n.value || face.face_id; return n || face.face_id; }; // Use internal displayTime for window calculations const windowStart = displayTime - 10; const windowEnd = displayTime + 10; // Memoize the processed segments to avoid heavy recalc on every frame // We only need to recalc when faces data changes, NOT on every currentTime update // However, filtering depends on currentTime (now displayTime) // Optimization: Pre-process all segments once, then filter efficiently. const allRows = useMemo(() => { if (!faces) return []; return faces.map(face => ({ id: face.face_id, name: getName(face), color: getColor(face.face_id), segments: face.screen_time.map(s => ({ start: parseTime(s.start) || 0, end: parseTime(s.end) || 0 })) })); }, [faces]); // Filter visible rows by determining overlap with current window const visibleRows = allRows.filter(row => { return row.segments.some(seg => (seg.start < windowEnd && seg.end > windowStart) ); }); // Sort: Maybe sort by who appears closest to now? or just stable ID? // Stable sort is better to prevent jumping. Keep original order. return (