"use client"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Video, Music, TypeIcon, Eye, VolumeOff, Volume2 } from "lucide-react"; import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger, } from "../../ui/context-menu"; import { useTimelineStore } from "@/stores/timeline-store"; import { usePlaybackStore } from "@/stores/playback-store"; import { useTimelineZoom } from "@/hooks/timeline/use-timeline-zoom"; import { useState, useRef, useEffect, useCallback } from "react"; import { TimelineTrackContent } from "./timeline-track"; import { TimelinePlayhead, useTimelinePlayheadRuler, } from "./timeline-playhead"; import { SelectionBox } from "../selection-box"; import { useSelectionBox } from "@/hooks/use-selection-box"; import { SnapIndicator } from "../snap-indicator"; import { SnapPoint } from "@/hooks/timeline/use-timeline-snapping"; import type { TimelineTrack } from "@/types/timeline"; import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; import { getTrackHeight, getCumulativeHeightBefore, getTotalTracksHeight, } from "@/lib/timeline"; import { TimelineToolbar } from "./timeline-toolbar"; import { useScrollSync } from "@/hooks/use-scroll-sync"; import { useTimelineInteractions } from "@/hooks/timeline/use-timeline-interactions"; import { useTimelineDragDrop } from "@/hooks/timeline/use-timeline-drag-drop"; import { TimelineRuler } from "./timeline-ruler"; export function Timeline() { const { tracks, getTotalDuration, clearSelectedElements, snappingEnabled, setSelectedElements, toggleTrackMute, dragState, } = useTimelineStore(); const { currentTime, duration, seek, setDuration } = usePlaybackStore(); const timelineRef = useRef(null); const rulerRef = useRef(null); const [isInTimeline, setIsInTimeline] = useState(false); // Timeline zoom functionality const { zoomLevel, setZoomLevel, handleWheel } = useTimelineZoom({ containerRef: timelineRef, isInTimeline, }); const { dragProps } = useTimelineDragDrop({ zoomLevel, }); // Dynamic timeline width calculation based on playhead position and duration const dynamicTimelineWidth = Math.max( (duration || 0) * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel, (currentTime + TIMELINE_CONSTANTS.PLAYHEAD_LOOKAHEAD_SECONDS) * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel, timelineRef.current?.clientWidth || 1000, ); // Scroll synchronization and auto-scroll to playhead const rulerScrollRef = useRef(null); const tracksScrollRef = useRef(null); const trackLabelsRef = useRef(null); const playheadRef = useRef(null); const trackLabelsScrollRef = useRef(null); // Timeline playhead ruler handlers const { handleRulerMouseDown } = useTimelinePlayheadRuler({ currentTime, duration, zoomLevel, seek, rulerRef, rulerScrollRef, tracksScrollRef, playheadRef, }); // Selection box functionality const tracksContainerRef = useRef(null); const { selectionBox, handleMouseDown: handleSelectionMouseDown, isSelecting, justFinishedSelecting, } = useSelectionBox({ containerRef: tracksContainerRef, playheadRef, onSelectionComplete: (elements) => { console.log(JSON.stringify({ onSelectionComplete: elements.length })); setSelectedElements(elements); }, }); // Calculate snap indicator state const [currentSnapPoint, setCurrentSnapPoint] = useState( null, ); const showSnapIndicator = dragState.isDragging && snappingEnabled && currentSnapPoint !== null; // Callback to handle snap point changes from TimelineTrackContent const handleSnapPointChange = useCallback((snapPoint: SnapPoint | null) => { setCurrentSnapPoint(snapPoint); }, []); const { handleTimelineMouseDown, handleTimelineContentClick } = useTimelineInteractions({ playheadRef, tracksContainerRef, rulerScrollRef, tracksScrollRef, zoomLevel, duration, isSelecting, justFinishedSelecting, clearSelectedElements, seek, }); // Update timeline duration when tracks change useEffect(() => { const totalDuration = getTotalDuration(); setDuration( Math.max(totalDuration, TIMELINE_CONSTANTS.MIN_DURATION_SECONDS), ); }, [tracks, setDuration, getTotalDuration]); // --- Scroll synchronization effect --- useScrollSync({ rulerScrollRef, tracksScrollRef, trackLabelsScrollRef, }); return (
setIsInTimeline(true)} onMouseLeave={() => setIsInTimeline(false)} >
{/* Timeline Header with Ruler */}
{/* Track Labels Header */}
{/* Empty space */} .
{/* Timeline Ruler */}
{/* Tracks Area */}
{/* Track Labels */} {tracks.length > 0 && (
{tracks.map((track) => (
{track.muted ? ( toggleTrackMute(track.id)} /> ) : ( toggleTrackMute(track.id)} /> )}
))}
)} {/* Timeline Tracks Content */}
{ // Check if this is horizontal scrolling - if so, don't handle it here if (e.shiftKey || Math.abs(e.deltaX) > Math.abs(e.deltaY)) { return; // Let ScrollArea handle horizontal scrolling } handleWheel(e); }} onMouseDown={(e) => { handleTimelineMouseDown(e); handleSelectionMouseDown(e); }} onClick={handleTimelineContentClick} ref={tracksContainerRef} >
{tracks.length === 0 ? (
) : ( <> {tracks.map((track, index) => (
{ // If clicking empty area (not on a element), deselect all elements if ( !(e.target as HTMLElement).closest( ".timeline-element", ) ) { clearSelectedElements(); } }} >
{ e.stopPropagation(); toggleTrackMute(track.id); }} > {track.muted ? "Unmute Track" : "Mute Track"} e.stopPropagation()}> Track settings (soon)
))} )}
); } function TrackIcon({ track }: { track: TimelineTrack }) { return ( <> {track.type === "media" && (