"use client"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Eye, EyeOff, VolumeOff, Volume2, LucideIcon, EyeIcon, Trash2, } from "lucide-react"; import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger, } from "../../ui/context-menu"; import { useTimelineZoom } from "@/hooks/timeline/use-timeline-zoom"; import { useState, useRef, useCallback } from "react"; import { TimelineTrackContent } from "./timeline-track"; import { TimelinePlayhead } from "./timeline-playhead"; import { SelectionBox } from "../selection-box"; import { useSelectionBox } from "@/hooks/timeline/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, TRACK_ICONS, } from "@/constants/timeline-constants"; import { useElementInteraction } from "@/hooks/timeline/element/use-element-interaction"; import { getTrackHeight, getCumulativeHeightBefore, getTotalTracksHeight, canTracktHaveAudio, canTrackBeHidden, isMainTrack, getTimelineZoomMin, } from "@/lib/timeline"; import { TimelineToolbar } from "./timeline-toolbar"; import { useScrollSync } from "@/hooks/timeline/use-scroll-sync"; import { useElementSelection } from "@/hooks/timeline/element/use-element-selection"; import { useTimelineInteractions } from "@/hooks/timeline/use-timeline-interactions"; import { useTimelineDragDrop } from "@/hooks/timeline/use-timeline-drag-drop"; import { TimelineRuler } from "./timeline-ruler"; import { TimelineBookmarksRow } from "./bookmarks"; import { useTimelineStore } from "@/stores/timeline-store"; import { useEditor } from "@/hooks/use-editor"; import { useTimelinePlayhead } from "@/hooks/timeline/use-timeline-playhead"; import { DragLine } from "./drag-line"; export function Timeline() { const tracksContainerHeight = { min: 0, max: 800 }; const { snappingEnabled } = useTimelineStore(); const { clearElementSelection, setElementSelection } = useElementSelection(); const editor = useEditor(); const timeline = editor.timeline; const tracks = timeline.getTracks(); const seek = (time: number) => editor.playback.seek({ time }); // refs const timelineRef = useRef(null); const rulerRef = useRef(null); const tracksContainerRef = useRef(null); const rulerScrollRef = useRef(null); const tracksScrollRef = useRef(null); const trackLabelsRef = useRef(null); const playheadRef = useRef(null); const trackLabelsScrollRef = useRef(null); const bookmarksScrollRef = useRef(null); // state const [isInTimeline, setIsInTimeline] = useState(false); const [isResizing, setIsResizing] = useState(false); const [currentSnapPoint, setCurrentSnapPoint] = useState( null, ); const handleSnapPointChange = useCallback((snapPoint: SnapPoint | null) => { setCurrentSnapPoint(snapPoint); }, []); const handleResizeStateChange = useCallback( ({ isResizing: nextIsResizing }: { isResizing: boolean }) => { setIsResizing(nextIsResizing); if (!nextIsResizing) { setCurrentSnapPoint(null); } }, [], ); const timelineDuration = timeline.getTotalDuration() || 0; const minZoomLevel = getTimelineZoomMin({ duration: timelineDuration, containerWidth: timelineRef.current?.clientWidth, }); const { zoomLevel, setZoomLevel, handleWheel } = useTimelineZoom({ containerRef: timelineRef, isInTimeline, minZoom: minZoomLevel, }); const { dragState, dragDropTarget, handleElementMouseDown, handleElementClick, lastMouseXRef, } = useElementInteraction({ zoomLevel, timelineRef, tracksContainerRef, tracksScrollRef, snappingEnabled, onSnapPointChange: handleSnapPointChange, }); const { handleRulerMouseDown: handlePlayheadRulerMouseDown } = useTimelinePlayhead({ zoomLevel, rulerRef, rulerScrollRef, tracksScrollRef, playheadRef, }); const { isDragOver, dropTarget, dragProps } = useTimelineDragDrop({ containerRef: tracksContainerRef, zoomLevel, }); const { selectionBox, handleMouseDown: handleSelectionMouseDown, isSelecting, } = useSelectionBox({ containerRef: tracksContainerRef, onSelectionComplete: (elements) => { setElementSelection({ elements }); }, tracksScrollRef, zoomLevel, }); const paddedDuration = timelineDuration + TIMELINE_CONSTANTS.PLAYHEAD_LOOKAHEAD_SECONDS; const dynamicTimelineWidth = Math.max( paddedDuration * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel, timelineRef.current?.clientWidth || 1000, ); const showSnapIndicator = snappingEnabled && currentSnapPoint !== null && (dragState.isDragging || isResizing); const { handleTracksMouseDown, handleTracksClick, handleRulerMouseDown, handleRulerClick, } = useTimelineInteractions({ playheadRef, trackLabelsRef, rulerScrollRef, tracksScrollRef, zoomLevel, duration: timeline.getTotalDuration(), isSelecting, clearSelectedElements: clearElementSelection, seek, }); useScrollSync({ rulerScrollRef, tracksScrollRef, trackLabelsScrollRef, bookmarksScrollRef, }); return (
setIsInTimeline(true)} onMouseLeave={() => setIsInTimeline(false)} > setZoomLevel(zoom)} />
.
.
{tracks.length > 0 && (
{tracks.map((track) => (
{/* Debug main track */} {/* {isMainTrack(track) && (
)} */} {canTracktHaveAudio(track) && ( editor.timeline.toggleTrackMute({ trackId: track.id, }) } /> )} {canTrackBeHidden(track) && ( editor.timeline.toggleTrackVisibility({ trackId: track.id, }) } /> )}
))}
)}
{ if (e.shiftKey || Math.abs(e.deltaX) > Math.abs(e.deltaY)) { return; } handleWheel(e); }} onMouseDown={(e) => { handleTracksMouseDown(e); handleSelectionMouseDown(e); }} onClick={handleTracksClick} ref={tracksContainerRef} > { const isDirectTarget = event.target === event.currentTarget; if (!isDirectTarget) return; event.stopPropagation(); handleTracksMouseDown(event); handleSelectionMouseDown(event); }} onClick={(event) => { const isDirectTarget = event.target === event.currentTarget; if (!isDirectTarget) return; event.stopPropagation(); handleTracksClick(event); }} >
{tracks.length === 0 ? (
) : ( <> {tracks.map((track, index) => (
{ e.stopPropagation(); timeline.toggleTrackMute({ trackId: track.id, }); }} > {canTracktHaveAudio(track) && track.muted ? "Unmute track" : "Mute track"} { e.stopPropagation(); timeline.toggleTrackVisibility({ trackId: track.id, }); }} > <> {canTrackBeHidden(track) && track.hidden ? "Show track" : "Hide track"} { e.stopPropagation(); timeline.removeTrack({ trackId: track.id, }); }} variant="destructive" > Delete track
))} )}
); } function TrackIcon({ track }: { track: TimelineTrack }) { return <>{TRACK_ICONS[track.type]}; } function TrackToggleIcon({ isOff, icons, onClick, }: { isOff: boolean; icons: { on: LucideIcon; off: LucideIcon; }; onClick: () => void; }) { return ( <> {isOff ? ( ) : ( )} ); }