mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
feat: move/resize multiple elements
This commit is contained in:
@@ -61,7 +61,6 @@ import { SELECTED_TRACK_ROW_CLASS } from "./theme";
|
||||
import {
|
||||
computeTrackExpansionHeight,
|
||||
getTrackExpandedRows,
|
||||
getExpansionHeight,
|
||||
getPropertyLabel,
|
||||
type ExpandedRow,
|
||||
} from "./expanded-layout";
|
||||
@@ -75,6 +74,7 @@ import { TimelineBookmarksRow } from "./bookmarks";
|
||||
import { useBookmarkDrag } from "@/hooks/timeline/use-bookmark-drag";
|
||||
import { useEdgeAutoScroll } from "@/hooks/timeline/use-edge-auto-scroll";
|
||||
import { useInitialScrollBottom } from "@/hooks/timeline/use-initial-scroll-bottom";
|
||||
import { useTimelineResize } from "@/hooks/timeline/use-timeline-resize";
|
||||
import { useTimelineStore } from "@/stores/timeline-store";
|
||||
import { useEditor } from "@/hooks/use-editor";
|
||||
import { useTimelinePlayhead } from "@/hooks/timeline/use-timeline-playhead";
|
||||
@@ -119,7 +119,9 @@ export function Timeline() {
|
||||
} = useElementSelection();
|
||||
const editor = useEditor();
|
||||
const timeline = editor.timeline;
|
||||
const scene = useEditor((currentEditor) => currentEditor.scenes.getActiveSceneOrNull());
|
||||
const scene = useEditor((currentEditor) =>
|
||||
currentEditor.scenes.getActiveSceneOrNull(),
|
||||
);
|
||||
const tracks = useMemo<TimelineTrack[]>(
|
||||
() =>
|
||||
scene
|
||||
@@ -140,7 +142,6 @@ export function Timeline() {
|
||||
const playheadRef = useRef<HTMLDivElement>(null);
|
||||
const trackLabelsScrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const [currentSnapPoint, setCurrentSnapPoint] = useState<SnapPoint | null>(
|
||||
null,
|
||||
);
|
||||
@@ -148,15 +149,6 @@ export function Timeline() {
|
||||
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({
|
||||
@@ -176,6 +168,10 @@ export function Timeline() {
|
||||
tracksScrollRef,
|
||||
rulerScrollRef,
|
||||
});
|
||||
const { isResizing, handleResizeStart } = useTimelineResize({
|
||||
zoomLevel,
|
||||
onSnapPointChange: handleSnapPointChange,
|
||||
});
|
||||
|
||||
const expandedElementIds = useTimelineStore((s) => s.expandedElementIds);
|
||||
|
||||
@@ -366,7 +362,10 @@ export function Timeline() {
|
||||
|
||||
const containerWidth =
|
||||
tracksContainerRef.current?.clientWidth || FALLBACK_CONTAINER_WIDTH;
|
||||
const contentWidth = timelineTimeToPixels({ time: timelineDuration, zoomLevel });
|
||||
const contentWidth = timelineTimeToPixels({
|
||||
time: timelineDuration,
|
||||
zoomLevel,
|
||||
});
|
||||
const paddingPx = getTimelinePaddingPx({
|
||||
containerWidth,
|
||||
zoomLevel,
|
||||
@@ -513,7 +512,10 @@ export function Timeline() {
|
||||
TRACKS_CONTAINER_HEIGHT.min,
|
||||
Math.min(
|
||||
TRACKS_CONTAINER_HEIGHT.max,
|
||||
getTotalTracksHeight({ tracks, getExtraHeight: getTrackExpansionHeight }),
|
||||
getTotalTracksHeight({
|
||||
tracks,
|
||||
getExtraHeight: getTrackExpansionHeight,
|
||||
}),
|
||||
),
|
||||
) + TIMELINE_CONTENT_TOP_PADDING_PX
|
||||
}px`,
|
||||
@@ -533,14 +535,13 @@ export function Timeline() {
|
||||
}}
|
||||
>
|
||||
{tracks.length > 0 && (
|
||||
<TimelineTrackRows
|
||||
mainTrackId={mainTrackId}
|
||||
<TimelineTrackRows
|
||||
mainTrackId={mainTrackId}
|
||||
zoomLevel={zoomLevel}
|
||||
dragState={dragState}
|
||||
tracksScrollRef={tracksScrollRef}
|
||||
lastMouseXRef={lastMouseXRef}
|
||||
onSnapPointChange={handleSnapPointChange}
|
||||
onResizeStateChange={handleResizeStateChange}
|
||||
onResizeStart={handleResizeStart}
|
||||
onElementMouseDown={handleElementMouseDown}
|
||||
onElementClick={handleElementClick}
|
||||
onTrackMouseDown={(event) => {
|
||||
@@ -718,8 +719,7 @@ function TimelineTrackRows({
|
||||
dragState,
|
||||
tracksScrollRef,
|
||||
lastMouseXRef,
|
||||
onSnapPointChange,
|
||||
onResizeStateChange,
|
||||
onResizeStart,
|
||||
onElementMouseDown,
|
||||
onElementClick,
|
||||
onTrackMouseDown,
|
||||
@@ -733,8 +733,9 @@ function TimelineTrackRows({
|
||||
dragState: ElementDragState;
|
||||
tracksScrollRef: React.RefObject<HTMLDivElement | null>;
|
||||
lastMouseXRef: React.RefObject<number>;
|
||||
onSnapPointChange: (snapPoint: SnapPoint | null) => void;
|
||||
onResizeStateChange: (params: { isResizing: boolean }) => void;
|
||||
onResizeStart: React.ComponentProps<
|
||||
typeof TimelineTrackContent
|
||||
>["onResizeStart"];
|
||||
onElementMouseDown: React.ComponentProps<
|
||||
typeof TimelineTrackContent
|
||||
>["onElementMouseDown"];
|
||||
@@ -798,8 +799,7 @@ function TimelineTrackRows({
|
||||
<div
|
||||
className={cn(
|
||||
"absolute right-0 left-0 transition-colors",
|
||||
tracksWithSelection.has(track.id) &&
|
||||
SELECTED_TRACK_ROW_CLASS,
|
||||
tracksWithSelection.has(track.id) && SELECTED_TRACK_ROW_CLASS,
|
||||
)}
|
||||
style={{
|
||||
top: `${TIMELINE_CONTENT_TOP_PADDING_PX + getCumulativeHeightBefore({ tracks, trackIndex: index, getExtraHeight: getTrackExpansionHeight })}px`,
|
||||
@@ -813,8 +813,7 @@ function TimelineTrackRows({
|
||||
rulerScrollRef={tracksScrollRef}
|
||||
tracksScrollRef={tracksScrollRef}
|
||||
lastMouseXRef={lastMouseXRef}
|
||||
onSnapPointChange={onSnapPointChange}
|
||||
onResizeStateChange={onResizeStateChange}
|
||||
onResizeStart={onResizeStart}
|
||||
onElementMouseDown={onElementMouseDown}
|
||||
onElementClick={onElementClick}
|
||||
onTrackMouseDown={onTrackMouseDown}
|
||||
@@ -929,12 +928,10 @@ function TrackToggleIcon({
|
||||
function PropertyTree({ rows }: { rows: ExpandedRow[] }) {
|
||||
return (
|
||||
<div className="flex flex-col overflow-hidden">
|
||||
{rows.map((row, index) => (
|
||||
{rows.map((row) => (
|
||||
<div
|
||||
key={row.propertyPath}
|
||||
className={cn(
|
||||
"flex shrink-0 items-center px-3 bg-muted/50",
|
||||
)}
|
||||
className={cn("flex shrink-0 items-center px-3 bg-muted/50")}
|
||||
style={{ height: `${KEYFRAME_LANE_HEIGHT_PX}px` }}
|
||||
>
|
||||
<span className="text-muted-foreground truncate text-xs leading-none">
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
} from "@/hooks/timeline/element/use-keyframe-drag";
|
||||
import { useKeyframeSelection } from "@/hooks/timeline/element/use-keyframe-selection";
|
||||
import { useKeyframeBoxSelect } from "@/hooks/timeline/element/use-keyframe-box-select";
|
||||
import { useTimelineElementResize } from "@/hooks/timeline/element/use-element-resize";
|
||||
import { SelectionBox } from "@/lib/selection/selection-box";
|
||||
import { getElementKeyframes } from "@/lib/animation";
|
||||
import {
|
||||
@@ -86,7 +85,6 @@ import {
|
||||
getExpansionHeight,
|
||||
type ExpandedRow,
|
||||
} from "./expanded-layout";
|
||||
import type { SnapPoint } from "@/lib/timeline/snap-utils";
|
||||
|
||||
const KEYFRAME_INDICATOR_MIN_WIDTH_PX = 40;
|
||||
const ELEMENT_RING_WIDTH_PX = 1.5;
|
||||
@@ -197,8 +195,12 @@ interface TimelineElementProps {
|
||||
track: TimelineTrack;
|
||||
zoomLevel: number;
|
||||
isSelected: boolean;
|
||||
onSnapPointChange?: (snapPoint: SnapPoint | null) => void;
|
||||
onResizeStateChange?: (params: { isResizing: boolean }) => void;
|
||||
onResizeStart: (params: {
|
||||
event: React.MouseEvent;
|
||||
element: TimelineElementType;
|
||||
track: TimelineTrack;
|
||||
side: "left" | "right";
|
||||
}) => void;
|
||||
onElementMouseDown: (
|
||||
event: React.MouseEvent,
|
||||
element: TimelineElementType,
|
||||
@@ -216,8 +218,7 @@ export function TimelineElement({
|
||||
track,
|
||||
zoomLevel,
|
||||
isSelected,
|
||||
onSnapPointChange,
|
||||
onResizeStateChange,
|
||||
onResizeStart,
|
||||
onElementMouseDown,
|
||||
onElementClick,
|
||||
dragState,
|
||||
@@ -231,18 +232,6 @@ export function TimelineElement({
|
||||
elementId: element.id,
|
||||
fallback: element,
|
||||
});
|
||||
const {
|
||||
currentDuration,
|
||||
currentStartTime,
|
||||
handleResizeStart,
|
||||
isResizing,
|
||||
} = useTimelineElementResize({
|
||||
element,
|
||||
track,
|
||||
zoomLevel,
|
||||
onSnapPointChange,
|
||||
onResizeStateChange,
|
||||
});
|
||||
|
||||
let mediaAsset: MediaAsset | null = null;
|
||||
|
||||
@@ -267,13 +256,9 @@ export function TimelineElement({
|
||||
const elementStartTime =
|
||||
isBeingDragged && dragState.isDragging
|
||||
? dragState.currentTime + dragTimeOffset
|
||||
: isResizing
|
||||
? currentStartTime
|
||||
: renderElement.startTime;
|
||||
: renderElement.startTime;
|
||||
const displayedStartTime = elementStartTime;
|
||||
const displayedDuration = isResizing
|
||||
? currentDuration
|
||||
: renderElement.duration;
|
||||
const displayedDuration = renderElement.duration;
|
||||
const elementWidth = timelineTimeToPixels({
|
||||
time: displayedDuration,
|
||||
zoomLevel,
|
||||
@@ -282,22 +267,6 @@ export function TimelineElement({
|
||||
time: displayedStartTime,
|
||||
zoomLevel,
|
||||
});
|
||||
const handleElementResizeStart = ({
|
||||
event,
|
||||
element,
|
||||
side,
|
||||
}: {
|
||||
event: React.MouseEvent;
|
||||
element: TimelineElementType;
|
||||
track: TimelineTrack;
|
||||
side: "left" | "right";
|
||||
}) => {
|
||||
handleResizeStart({
|
||||
event,
|
||||
elementId: element.id,
|
||||
side,
|
||||
});
|
||||
};
|
||||
const keyframeIndicators = isSelected
|
||||
? getKeyframeIndicators({
|
||||
keyframes: getElementKeyframes({ animations: element.animations }),
|
||||
@@ -329,9 +298,7 @@ export function TimelineElement({
|
||||
);
|
||||
const expandedRows = useMemo(
|
||||
() =>
|
||||
isExpanded
|
||||
? getExpandedRows({ animations: element.animations })
|
||||
: [],
|
||||
isExpanded ? getExpandedRows({ animations: element.animations }) : [],
|
||||
[isExpanded, element.animations],
|
||||
);
|
||||
|
||||
@@ -422,7 +389,7 @@ export function TimelineElement({
|
||||
expandedContent={expandedContent}
|
||||
onElementClick={onElementClick}
|
||||
onElementMouseDown={onElementMouseDown}
|
||||
onResizeStart={handleElementResizeStart}
|
||||
onResizeStart={onResizeStart}
|
||||
isDropTarget={isDropTarget}
|
||||
/>
|
||||
{isSelected && (
|
||||
@@ -811,8 +778,7 @@ function ExpandedKeyframeLanes({
|
||||
[...keyframes]
|
||||
.sort(
|
||||
(a, b) =>
|
||||
a.time - b.time ||
|
||||
a.propertyPath.localeCompare(b.propertyPath),
|
||||
a.time - b.time || a.propertyPath.localeCompare(b.propertyPath),
|
||||
)
|
||||
.map((kf) => ({
|
||||
trackId,
|
||||
@@ -824,6 +790,8 @@ function ExpandedKeyframeLanes({
|
||||
);
|
||||
|
||||
return (
|
||||
// biome-ignore lint/a11y/noStaticElementInteractions: expanded keyframe lanes are a pointer-only editing surface
|
||||
// biome-ignore lint/a11y/useKeyWithClickEvents: expanded keyframe lanes are a pointer-only editing surface
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="relative flex flex-col"
|
||||
@@ -837,9 +805,7 @@ function ExpandedKeyframeLanes({
|
||||
return (
|
||||
<div
|
||||
key={row.propertyPath}
|
||||
className={cn(
|
||||
"relative flex items-center bg-muted/50",
|
||||
)}
|
||||
className={cn("relative flex items-center bg-muted/50")}
|
||||
style={{ height: `${KEYFRAME_LANE_HEIGHT_PX}px` }}
|
||||
>
|
||||
{laneKeyframes.map((kf) => {
|
||||
@@ -849,8 +815,9 @@ function ExpandedKeyframeLanes({
|
||||
propertyPath: row.propertyPath,
|
||||
keyframeId: kf.id,
|
||||
};
|
||||
const isBeingDragged =
|
||||
keyframeDragState.draggingKeyframeIds.has(kf.id);
|
||||
const isBeingDragged = keyframeDragState.draggingKeyframeIds.has(
|
||||
kf.id,
|
||||
);
|
||||
const kfLeft = timelineTimeToSnappedPixels({
|
||||
time: displayedStartTime + kf.time,
|
||||
zoomLevel,
|
||||
@@ -898,9 +865,7 @@ function ExpandedKeyframeLanes({
|
||||
icon={KeyframeIcon}
|
||||
className={cn(
|
||||
"size-3.5 text-black mr-1",
|
||||
isSelected
|
||||
? "fill-primary"
|
||||
: "fill-white",
|
||||
isSelected ? "fill-primary" : "fill-white",
|
||||
)}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user