summaryrefslogtreecommitdiff
path: root/apps/web/src/components/editor/panels/timeline/timeline-track.tsx
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-15 05:43:29 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-15 05:43:29 +0200
commitac43a5a73fa905cdac905bc956763b6093d6738e (patch)
tree8d9ff8c3a9ae55bac8860215bec69cc5b6094a1c /apps/web/src/components/editor/panels/timeline/timeline-track.tsx
parent6312fc5b078689bc82c52fda9b7f92ee0a21d311 (diff)
downloadopencut-ac43a5a73fa905cdac905bc956763b6093d6738e.tar.gz
feat: move/resize multiple elements
Diffstat (limited to 'apps/web/src/components/editor/panels/timeline/timeline-track.tsx')
-rw-r--r--apps/web/src/components/editor/panels/timeline/timeline-track.tsx261
1 files changed, 132 insertions, 129 deletions
diff --git a/apps/web/src/components/editor/panels/timeline/timeline-track.tsx b/apps/web/src/components/editor/panels/timeline/timeline-track.tsx
index 888e1496..32b1b93d 100644
--- a/apps/web/src/components/editor/panels/timeline/timeline-track.tsx
+++ b/apps/web/src/components/editor/panels/timeline/timeline-track.tsx
@@ -1,129 +1,132 @@
-"use client";
-
-import { useElementSelection } from "@/hooks/timeline/element/use-element-selection";
-import { TimelineElement } from "./timeline-element";
-import type { TimelineTrack } from "@/lib/timeline";
-import type { TimelineElement as TimelineElementType } from "@/lib/timeline";
-import type { SnapPoint } from "@/lib/timeline/snap-utils";
-import { TIMELINE_LAYERS } from "./layers";
-import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/lib/timeline/scale";
-import { useEdgeAutoScroll } from "@/hooks/timeline/use-edge-auto-scroll";
-import type { ElementDragState } from "@/lib/timeline";
-import { useEditor } from "@/hooks/use-editor";
-
-interface TimelineTrackContentProps {
- track: TimelineTrack;
- zoomLevel: number;
- dragState: ElementDragState;
- rulerScrollRef: React.RefObject<HTMLDivElement | null>;
- tracksScrollRef: React.RefObject<HTMLDivElement | null>;
- lastMouseXRef: React.RefObject<number>;
- onSnapPointChange?: (snapPoint: SnapPoint | null) => void;
- onResizeStateChange?: (params: { isResizing: boolean }) => 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,
- onSnapPointChange,
- onResizeStateChange,
- 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}
- onSnapPointChange={onSnapPointChange}
- onResizeStateChange={onResizeStateChange}
- onElementMouseDown={(event, element) =>
- onElementMouseDown({ event, element, track })
- }
- onElementClick={(event, element) =>
- onElementClick({ event, element, track })
- }
- dragState={dragState}
- isDropTarget={element.id === targetElementId}
- />
- );
- })
- )}
- </div>
- </div>
- );
-}
+"use client";
+
+import { useElementSelection } from "@/hooks/timeline/element/use-element-selection";
+import { TimelineElement } from "./timeline-element";
+import type { TimelineTrack } from "@/lib/timeline";
+import type { TimelineElement as TimelineElementType } from "@/lib/timeline";
+import { TIMELINE_LAYERS } from "./layers";
+import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/lib/timeline/scale";
+import { useEdgeAutoScroll } from "@/hooks/timeline/use-edge-auto-scroll";
+import type { ElementDragState } from "@/lib/timeline";
+import { useEditor } from "@/hooks/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>
+ );
+}