diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-04-20 11:31:17 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-04-20 11:31:17 +0200 |
| commit | 3e89d2998588f8e35c524e523ba2ee853f0f6475 (patch) | |
| tree | 57221f96c1259b3b07e3ddf8c1adefc502b9d8eb /apps/web/src/timeline/components/timeline-track.tsx | |
| parent | 729d10592f4e802f91c555c309ff3ba4b119f843 (diff) | |
| download | opencut-3e89d2998588f8e35c524e523ba2ee853f0f6475.tar.gz | |
refactor: restructure files to their domains, new preview overlay system, and dep graph
Diffstat (limited to 'apps/web/src/timeline/components/timeline-track.tsx')
| -rw-r--r-- | apps/web/src/timeline/components/timeline-track.tsx | 132 |
1 files changed, 132 insertions, 0 deletions
diff --git a/apps/web/src/timeline/components/timeline-track.tsx b/apps/web/src/timeline/components/timeline-track.tsx new file mode 100644 index 00000000..6c40637e --- /dev/null +++ b/apps/web/src/timeline/components/timeline-track.tsx @@ -0,0 +1,132 @@ +"use client"; + +import { useElementSelection } from "@/timeline/hooks/element/use-element-selection"; +import { TimelineElement } from "./timeline-element"; +import type { TimelineTrack } from "@/timeline"; +import type { TimelineElement as TimelineElementType } from "@/timeline"; +import { TIMELINE_LAYERS } from "./layers"; +import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/timeline/scale"; +import { useEdgeAutoScroll } from "@/timeline/hooks/use-edge-auto-scroll"; +import type { ElementDragState } from "@/timeline"; +import { useEditor } from "@/editor/use-editor"; + +interface TimelineTrackContentProps { + track: TimelineTrack; + zoomLevel: number; + dragState: ElementDragState; + rulerScrollRef: React.RefObject<HTMLDivElement | null>; + tracksScrollRef: React.RefObject<HTMLDivElement | null>; + lastMouseXRef: React.RefObject<number>; + onResizeStart: (params: { + event: React.MouseEvent; + element: TimelineElementType; + track: TimelineTrack; + side: "left" | "right"; + }) => void; + onElementMouseDown: (params: { + event: React.MouseEvent; + element: TimelineElementType; + track: TimelineTrack; + }) => void; + onElementClick: (params: { + event: React.MouseEvent; + element: TimelineElementType; + track: TimelineTrack; + }) => void; + onTrackMouseDown?: (event: React.MouseEvent) => void; + onTrackMouseUp?: (event: React.MouseEvent) => void; + shouldIgnoreClick?: () => boolean; + targetElementId?: string | null; +} + +export function TimelineTrackContent({ + track, + zoomLevel, + dragState, + rulerScrollRef, + tracksScrollRef, + lastMouseXRef, + onResizeStart, + onElementMouseDown, + onElementClick, + onTrackMouseDown, + onTrackMouseUp, + shouldIgnoreClick, + targetElementId = null, +}: TimelineTrackContentProps) { + const { isElementSelected } = useElementSelection(); + const duration = useEditor((e) => e.timeline.getTotalDuration()); + + useEdgeAutoScroll({ + isActive: dragState.isDragging, + getMouseClientX: () => lastMouseXRef.current ?? 0, + rulerScrollRef, + tracksScrollRef, + contentWidth: duration * BASE_TIMELINE_PIXELS_PER_SECOND * zoomLevel, + }); + + return ( + <div className="relative size-full"> + <button + type="button" + className="absolute inset-0 m-0 size-full appearance-none border-0 bg-transparent p-0" + aria-label={`Select ${track.name} track`} + onMouseUp={(event) => { + if (shouldIgnoreClick?.()) return; + onTrackMouseUp?.(event); + }} + onMouseDown={(event) => { + event.preventDefault(); + onTrackMouseDown?.(event); + }} + /> + {/* biome-ignore lint/a11y/noStaticElementInteractions: empty track area is a pointer-only seek surface */} + <div + className="relative h-full min-w-full" + style={{ zIndex: TIMELINE_LAYERS.trackContent }} + onMouseUp={(event) => { + if (event.target !== event.currentTarget) return; + if (shouldIgnoreClick?.()) return; + onTrackMouseUp?.(event); + }} + onMouseDown={(event) => { + if (event.target !== event.currentTarget) return; + event.preventDefault(); + onTrackMouseDown?.(event); + }} + > + {track.elements.length === 0 ? ( + <div className="text-muted-foreground border-muted/30 pointer-events-none flex size-full items-center justify-center rounded-sm border-2 border-dashed text-xs" /> + ) : ( + track.elements.map((element) => { + const isSelected = isElementSelected({ + trackId: track.id, + elementId: element.id, + }); + + return ( + <TimelineElement + key={element.id} + element={element} + track={track} + zoomLevel={zoomLevel} + isSelected={isSelected} + onResizeStart={({ event, element, side }) => + onResizeStart({ event, element, track, side }) + } + onElementMouseDown={(event, element) => + onElementMouseDown({ event, element, track }) + } + onElementClick={(event, element) => + onElementClick({ event, element, track }) + } + dragState={dragState} + isDropTarget={element.id === targetElementId} + /> + ); + }) + )} + </div> + </div> + ); +} |
