@hyperframes/studio 0.7.54 → 0.7.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{index-uBY329wb.js → index-BWnOxAiH.js} +1 -1
- package/dist/assets/{index-CMHYjEZ5.js → index-C4csZims.js} +1 -1
- package/dist/assets/index-CmVCjZjp.js +423 -0
- package/dist/assets/index-D-GyYi2d.css +1 -0
- package/dist/{chunk-SOTCF4DF.js → chunk-5QSIMBEJ.js} +2 -1
- package/dist/chunk-5QSIMBEJ.js.map +1 -0
- package/dist/{domEditingLayers-2ECJK24D.js → domEditingLayers-6LQGKPOI.js} +2 -2
- package/dist/index.d.ts +146 -144
- package/dist/index.html +2 -2
- package/dist/index.js +40582 -37803
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +106 -96
- package/src/components/EditorShell.tsx +253 -0
- package/src/components/StudioGlobalDragOverlay.tsx +1 -3
- package/src/components/StudioLeftSidebar.tsx +13 -3
- package/src/components/StudioRightPanel.tsx +8 -3
- package/src/components/TimelineToolbar.test.tsx +2 -2
- package/src/components/TimelineToolbar.tsx +179 -121
- package/src/components/editor/CanvasContextMenu.test.tsx +115 -0
- package/src/components/editor/CanvasContextMenu.tsx +198 -0
- package/src/components/editor/DomEditCropHandles.test.tsx +74 -3
- package/src/components/editor/DomEditCropHandles.tsx +64 -29
- package/src/components/editor/DomEditOverlay.test.ts +245 -279
- package/src/components/editor/DomEditOverlay.tsx +158 -126
- package/src/components/editor/DomEditOverlayHover.test.tsx +90 -0
- package/src/components/editor/DomEditRotateHandle.tsx +38 -18
- package/src/components/editor/DomEditSelectionChrome.test.tsx +80 -0
- package/src/components/editor/DomEditSelectionChrome.tsx +257 -0
- package/src/components/editor/LayersPanel.test.ts +66 -2
- package/src/components/editor/LayersPanel.tsx +43 -1
- package/src/components/editor/OffCanvasIndicators.test.tsx +34 -0
- package/src/components/editor/OffCanvasIndicators.tsx +49 -10
- package/src/components/editor/SnapGuideOverlay.tsx +28 -23
- package/src/components/editor/SnapToolbar.test.tsx +0 -1
- package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +214 -0
- package/src/components/editor/anchoredResizeReleaseShift.test.ts +208 -0
- package/src/components/editor/canvasContextMenuZOrder.test.ts +435 -0
- package/src/components/editor/canvasContextMenuZOrder.ts +352 -0
- package/src/components/editor/domEditNudge.test.ts +80 -0
- package/src/components/editor/domEditNudge.ts +44 -0
- package/src/components/editor/domEditOverlayGeometry.test.ts +167 -1
- package/src/components/editor/domEditOverlayGeometry.ts +280 -20
- package/src/components/editor/domEditOverlayGestures.ts +44 -41
- package/src/components/editor/domEditOverlayStartGesture.ts +62 -2
- package/src/components/editor/domEditResizeLocal.test.ts +131 -0
- package/src/components/editor/domEditResizeLocal.ts +125 -0
- package/src/components/editor/domEditingDom.ts +1 -1
- package/src/components/editor/manualEditsDomPatches.test.ts +47 -0
- package/src/components/editor/manualOffsetDrag.ts +55 -12
- package/src/components/editor/offCanvasIndicatorGeometry.ts +31 -10
- package/src/components/editor/offCanvasIndicatorRefresh.test.tsx +25 -1
- package/src/components/editor/resizeDraft.ts +108 -0
- package/src/components/editor/snapEngine.test.ts +12 -78
- package/src/components/editor/snapEngine.ts +13 -53
- package/src/components/editor/useDomEditNudge.test.tsx +228 -0
- package/src/components/editor/useDomEditNudge.ts +254 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +115 -116
- package/src/components/editor/useDomEditOverlayRects.ts +9 -2
- package/src/components/nle/AssetPreviewOverlay.tsx +147 -0
- package/src/components/nle/NLEContext.test.ts +144 -0
- package/src/components/nle/NLEContext.tsx +328 -0
- package/src/components/nle/NLEPreview.tsx +1 -1
- package/src/components/nle/PreviewOverlays.tsx +240 -0
- package/src/components/nle/PreviewPane.tsx +163 -0
- package/src/components/nle/TimelinePane.test.ts +60 -0
- package/src/components/nle/TimelinePane.tsx +293 -0
- package/src/components/nle/TimelineResizeDivider.tsx +8 -2
- package/src/components/nle/useCompositionStack.test.tsx +44 -0
- package/src/components/nle/useCompositionStack.ts +17 -9
- package/src/components/nle/useTimelineEditCallbacks.ts +217 -0
- package/src/components/sidebar/AssetCard.tsx +326 -0
- package/src/components/sidebar/AssetContextMenu.tsx +15 -30
- package/src/components/sidebar/AssetsTab.test.ts +96 -0
- package/src/components/sidebar/AssetsTab.tsx +71 -206
- package/src/components/sidebar/AudioRow.tsx +39 -2
- package/src/components/sidebar/BlocksTab.tsx +8 -1
- package/src/components/sidebar/LeftSidebar.tsx +4 -1
- package/src/components/sidebar/assetHelpers.ts +29 -0
- package/src/contexts/DomEditContext.tsx +9 -0
- package/src/contexts/StudioContext.tsx +9 -8
- package/src/contexts/TimelineEditContext.tsx +3 -7
- package/src/hooks/deleteSelectedKeyframes.ts +35 -0
- package/src/hooks/domEditCommitTypes.ts +12 -1
- package/src/hooks/domSelectionTestHarness.ts +14 -0
- package/src/hooks/gestureTransaction.test.ts +311 -0
- package/src/hooks/gestureTransaction.ts +199 -0
- package/src/hooks/gsapDragCommit.test.ts +141 -41
- package/src/hooks/gsapDragCommit.ts +64 -74
- package/src/hooks/gsapDragStaticSetHelpers.ts +9 -21
- package/src/hooks/gsapResizeIntercept.test.ts +69 -0
- package/src/hooks/gsapResizeIntercept.ts +38 -10
- package/src/hooks/gsapRuntimeBridge.test.ts +49 -13
- package/src/hooks/gsapRuntimeBridge.ts +4 -4
- package/src/hooks/gsapScriptCommitTypes.ts +16 -5
- package/src/hooks/gsapShared.test.ts +19 -1
- package/src/hooks/gsapShared.ts +14 -0
- package/src/hooks/patchDocumentRootDuration.test.ts +71 -0
- package/src/hooks/timelineEditingGsap.ts +14 -0
- package/src/hooks/timelineEditingHelpers.test.ts +34 -1
- package/src/hooks/timelineEditingHelpers.ts +22 -48
- package/src/hooks/timelineMoveAdapter.test.ts +77 -0
- package/src/hooks/timelineMoveAdapter.ts +37 -0
- package/src/hooks/useAnimatedPropertyCommit.test.tsx +36 -8
- package/src/hooks/useAnimatedPropertyCommit.ts +31 -26
- package/src/hooks/useAppHotkeys.ts +16 -25
- package/src/hooks/useBlockHandlers.ts +55 -27
- package/src/hooks/useContextMenuDismiss.ts +31 -7
- package/src/hooks/useDomEditCommits.test.tsx +154 -1
- package/src/hooks/useDomEditCommits.ts +100 -3
- package/src/hooks/useDomEditPositionPatchCommit.ts +2 -0
- package/src/hooks/useDomEditSession.ts +2 -2
- package/src/hooks/useDomEditTextCommits.test.tsx +135 -0
- package/src/hooks/useDomEditTextCommits.ts +8 -3
- package/src/hooks/useDomEditWiring.ts +7 -9
- package/src/hooks/useDomGeometryCommits.ts +17 -2
- package/src/hooks/useDomSelection.test.ts +2 -36
- package/src/hooks/useDomSelection.ts +81 -51
- package/src/hooks/useDomSelectionSelectionGuards.test.ts +276 -0
- package/src/hooks/useElementLifecycleOps.test.tsx +258 -0
- package/src/hooks/useElementLifecycleOps.ts +40 -71
- package/src/hooks/useEnableKeyframes.test.ts +142 -1
- package/src/hooks/useEnableKeyframes.ts +73 -29
- package/src/hooks/useGestureCommit.test.tsx +130 -0
- package/src/hooks/useGestureCommit.ts +32 -8
- package/src/hooks/useGroupCommits.ts +7 -28
- package/src/hooks/useGsapAwareEditing.test.tsx +239 -0
- package/src/hooks/useGsapAwareEditing.ts +115 -20
- package/src/hooks/useGsapKeyframeOps.test.tsx +59 -0
- package/src/hooks/useGsapKeyframeOps.ts +38 -10
- package/src/hooks/useGsapScriptCommits.test.tsx +40 -1
- package/src/hooks/useGsapScriptCommits.ts +134 -50
- package/src/hooks/useGsapSelectionHandlers.ts +49 -16
- package/src/hooks/useMusicBeatAnalysis.ts +72 -36
- package/src/hooks/usePersistentEditHistory.test.ts +33 -0
- package/src/hooks/usePersistentEditHistory.ts +92 -58
- package/src/hooks/usePreviewPersistence.ts +25 -7
- package/src/hooks/useRazorSplit.history.test.tsx +303 -0
- package/src/hooks/useRazorSplit.test.ts +269 -0
- package/src/hooks/useRazorSplit.testHelpers.ts +64 -0
- package/src/hooks/useRazorSplit.ts +107 -36
- package/src/hooks/useRenderClipContent.ts +24 -6
- package/src/hooks/useStudioContextValue.ts +15 -5
- package/src/hooks/useStudioUrlState.ts +2 -4
- package/src/hooks/useTimelineEditing.test.tsx +20 -29
- package/src/hooks/useTimelineEditingTypes.ts +6 -0
- package/src/hooks/useTimelineGroupEditing.ts +18 -4
- package/src/index.ts +1 -1
- package/src/player/components/ImageThumbnail.test.tsx +173 -0
- package/src/player/components/ImageThumbnail.tsx +160 -0
- package/src/player/components/PlayheadIndicator.tsx +50 -5
- package/src/player/components/Timeline.test.ts +102 -19
- package/src/player/components/Timeline.tsx +194 -194
- package/src/player/components/TimelineCanvas.tsx +207 -521
- package/src/player/components/TimelineClip.tsx +4 -1
- package/src/player/components/TimelineClipDiamonds.tsx +9 -40
- package/src/player/components/TimelineEmptyState.tsx +3 -1
- package/src/player/components/TimelineLanes.tsx +487 -0
- package/src/player/components/TimelineOverlays.tsx +122 -0
- package/src/player/components/VideoThumbnail.test.tsx +152 -0
- package/src/player/components/VideoThumbnail.tsx +28 -7
- package/src/player/components/thumbnailUtils.test.ts +127 -0
- package/src/player/components/thumbnailUtils.ts +54 -0
- package/src/player/components/timelineCallbacks.ts +18 -18
- package/src/player/components/timelineClipChildren.tsx +39 -0
- package/src/player/components/timelineClipDragCommit.test.ts +943 -0
- package/src/player/components/timelineClipDragCommit.ts +401 -0
- package/src/player/components/timelineClipDragPreview.test.ts +144 -0
- package/src/player/components/timelineClipDragPreview.ts +323 -38
- package/src/player/components/timelineClipDragTypes.ts +65 -0
- package/src/player/components/timelineCollision.test.ts +477 -0
- package/src/player/components/timelineCollision.ts +263 -0
- package/src/player/components/timelineDragDrop.ts +69 -60
- package/src/player/components/timelineEditCapabilities.ts +52 -0
- package/src/player/components/timelineEditing.test.ts +1 -1
- package/src/player/components/timelineEditing.ts +76 -59
- package/src/player/components/timelineGroupEditing.test.ts +126 -0
- package/src/player/components/timelineGroupEditing.ts +133 -0
- package/src/player/components/timelineGroupResizeCommit.ts +54 -0
- package/src/player/components/timelineLayout.test.ts +95 -23
- package/src/player/components/timelineLayout.ts +120 -61
- package/src/player/components/timelineMarquee.test.ts +197 -0
- package/src/player/components/timelineMarquee.ts +112 -0
- package/src/player/components/timelineMultiDragPreview.test.ts +127 -0
- package/src/player/components/timelineMultiDragPreview.ts +106 -0
- package/src/player/components/timelineOptimisticRevision.ts +42 -0
- package/src/player/components/timelineSnapping.test.ts +134 -0
- package/src/player/components/timelineSnapping.ts +110 -0
- package/src/player/components/timelineStackingSync.test.ts +244 -0
- package/src/player/components/timelineStackingSync.ts +331 -0
- package/src/player/components/timelineTheme.ts +6 -1
- package/src/player/components/timelineZones.test.ts +425 -0
- package/src/player/components/timelineZones.ts +198 -0
- package/src/player/components/timelineZoom.test.ts +67 -0
- package/src/player/components/timelineZoom.ts +51 -0
- package/src/player/components/useResolvedTimelineEditCallbacks.ts +25 -3
- package/src/player/components/useTimelineClipDrag.resize.test.tsx +241 -0
- package/src/player/components/useTimelineClipDrag.ts +380 -386
- package/src/player/components/useTimelineEditPinning.ts +121 -0
- package/src/player/components/useTimelineGeometry.ts +129 -0
- package/src/player/components/useTimelinePlayhead.ts +4 -16
- package/src/player/components/useTimelineRangeSelection.ts +341 -50
- package/src/player/components/useTimelineStackingSync.test.tsx +67 -0
- package/src/player/components/useTimelineStackingSync.ts +83 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +0 -3
- package/src/player/hooks/usePlaybackKeyboard.ts +5 -0
- package/src/player/hooks/useTimelinePlayer.ts +48 -53
- package/src/player/hooks/useTimelinePlayerLoop.ts +85 -0
- package/src/player/hooks/useTimelineSyncCallbacks.test.ts +219 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +104 -4
- package/src/player/index.ts +3 -2
- package/src/player/lib/time.test.ts +19 -19
- package/src/player/lib/time.ts +6 -2
- package/src/player/lib/timelineDOM.test.ts +25 -98
- package/src/player/lib/timelineDOM.ts +12 -81
- package/src/player/lib/timelineElementHelpers.ts +54 -5
- package/src/player/lib/timelineIframeHelpers.ts +2 -0
- package/src/player/store/playerStore.ts +62 -3
- package/src/styles/studio.css +27 -0
- package/src/utils/assetClickBehavior.test.ts +104 -0
- package/src/utils/assetClickBehavior.ts +75 -0
- package/src/utils/assetPreviewStore.ts +33 -0
- package/src/utils/blockInstaller.ts +10 -2
- package/src/utils/canvasNudgeGate.test.ts +31 -0
- package/src/utils/canvasNudgeGate.ts +32 -0
- package/src/utils/editHistory.test.ts +35 -0
- package/src/utils/gsapSoftReload.test.ts +119 -1
- package/src/utils/gsapSoftReload.ts +153 -0
- package/src/utils/mediaTypes.ts +3 -2
- package/src/utils/resizeDebug.ts +55 -0
- package/src/utils/rootDuration.test.ts +90 -1
- package/src/utils/rootDuration.ts +76 -13
- package/src/utils/sdkCutover.ts +4 -0
- package/src/utils/studioFileHistory.ts +37 -7
- package/src/utils/studioHelpers.ts +62 -0
- package/src/utils/studioPreviewHelpers.test.ts +65 -8
- package/src/utils/studioPreviewHelpers.ts +10 -0
- package/src/utils/studioTelemetry.ts +4 -1
- package/src/utils/studioUiPreferences.test.ts +38 -0
- package/src/utils/studioUiPreferences.ts +27 -0
- package/src/utils/studioUrlState.test.ts +0 -1
- package/src/utils/timelineAssetDrop.ts +62 -0
- package/src/utils/timelineDiscovery.ts +0 -17
- package/src/utils/timelineInspector.test.ts +121 -0
- package/src/utils/timelineInspector.ts +32 -1
- package/dist/assets/index-Dq7FEg0K.css +0 -1
- package/dist/assets/index-pRhCpGPz.js +0 -423
- package/dist/chunk-SOTCF4DF.js.map +0 -1
- package/src/components/StudioPreviewArea.tsx +0 -500
- package/src/components/nle/NLELayout.test.ts +0 -12
- package/src/components/nle/NLELayout.tsx +0 -591
- package/src/player/components/TimelineLayerGutter.tsx +0 -61
- package/src/player/components/TimelineSelectionOverlays.tsx +0 -55
- package/src/player/components/timelineMarqueeSelection.test.ts +0 -87
- package/src/player/components/timelineSnapTargets.test.ts +0 -144
- package/src/player/components/timelineSnapTargets.ts +0 -164
- package/src/player/components/useTimelineClipDrag.test.tsx +0 -506
- package/src/player/components/useTimelineClipGroupDrag.ts +0 -417
- package/src/player/components/useTimelineMarqueeSelection.test.tsx +0 -231
- package/src/player/components/useTimelineMarqueeSelection.ts +0 -276
- package/src/utils/timelineDiscovery.test.ts +0 -90
- /package/dist/{domEditingLayers-2ECJK24D.js.map → domEditingLayers-6LQGKPOI.js.map} +0 -0
|
@@ -2,6 +2,7 @@ import { memo, type CSSProperties, type ReactNode } from "react";
|
|
|
2
2
|
import type { TimelineElement } from "../store/playerStore";
|
|
3
3
|
import { defaultTimelineTheme, getClipHandleOpacity, type TimelineTheme } from "./timelineTheme";
|
|
4
4
|
import type { TimelineEditCapabilities } from "./timelineEditing";
|
|
5
|
+
import { isAudioTimelineElement } from "../../utils/timelineInspector";
|
|
5
6
|
|
|
6
7
|
interface TimelineClipProps {
|
|
7
8
|
el: TimelineElement;
|
|
@@ -62,6 +63,7 @@ export const TimelineClip = memo(function TimelineClip({
|
|
|
62
63
|
isHovered ? "is-hovered" : "",
|
|
63
64
|
isDragging ? "is-dragging" : "",
|
|
64
65
|
showDefaultText ? "" : "is-micro",
|
|
66
|
+
isAudioTimelineElement(el) ? "is-audio" : "",
|
|
65
67
|
]
|
|
66
68
|
.filter((className) => className.length > 0)
|
|
67
69
|
.join(" ");
|
|
@@ -72,7 +74,8 @@ export const TimelineClip = memo(function TimelineClip({
|
|
|
72
74
|
bottom: clipY,
|
|
73
75
|
borderRadius: theme.clipRadius,
|
|
74
76
|
zIndex: isDragging ? 20 : isSelected ? 10 : isHovered ? 5 : 1,
|
|
75
|
-
cursor
|
|
77
|
+
// Regular cursor over clips (CapCut-style, user preference) — no grab hand.
|
|
78
|
+
cursor: "default",
|
|
76
79
|
transform: isDragging ? "translateY(-1px)" : undefined,
|
|
77
80
|
};
|
|
78
81
|
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { memo, useRef, useState } from "react";
|
|
2
2
|
import { BEAT_BAND_H } from "./BeatStrip";
|
|
3
3
|
import {
|
|
4
|
-
clampToNeighbors,
|
|
5
4
|
KEYFRAME_DRAG_THRESHOLD_PX,
|
|
6
5
|
previewClipPct,
|
|
7
6
|
resolveKeyframeDrag,
|
|
8
7
|
} from "../../components/editor/keyframeDrag";
|
|
9
|
-
import { snapKeyframePctToBeat } from "./timelineEditing";
|
|
10
8
|
|
|
11
9
|
interface KeyframeEntry {
|
|
12
10
|
percentage: number;
|
|
@@ -30,14 +28,6 @@ interface TimelineClipDiamondsProps {
|
|
|
30
28
|
/** Beat-dot strip is shown on this track → shrink diamonds + drop them into
|
|
31
29
|
* the bottom half so they clear the strip at the top. */
|
|
32
30
|
beatsActive?: boolean;
|
|
33
|
-
/** Composition-time beat positions (same source the beat strip renders from).
|
|
34
|
-
* When present and `beatsActive`, a dragged keyframe snaps to the nearest beat. */
|
|
35
|
-
beatTimes?: number[];
|
|
36
|
-
/** Clip start / duration (seconds) + pixels-per-second, needed to map a
|
|
37
|
-
* dragged keyframe's clip-% to composition time for beat snapping. */
|
|
38
|
-
clipStart?: number;
|
|
39
|
-
clipDurationSeconds?: number;
|
|
40
|
-
pixelsPerSecond?: number;
|
|
41
31
|
accentColor: string;
|
|
42
32
|
isSelected: boolean;
|
|
43
33
|
currentPercentage: number;
|
|
@@ -81,10 +71,6 @@ export const TimelineClipDiamonds = memo(function TimelineClipDiamonds({
|
|
|
81
71
|
clipWidthPx,
|
|
82
72
|
clipHeightPx,
|
|
83
73
|
beatsActive,
|
|
84
|
-
beatTimes,
|
|
85
|
-
clipStart = 0,
|
|
86
|
-
clipDurationSeconds = 0,
|
|
87
|
-
pixelsPerSecond = 1,
|
|
88
74
|
accentColor,
|
|
89
75
|
isSelected,
|
|
90
76
|
currentPercentage,
|
|
@@ -135,20 +121,6 @@ export const TimelineClipDiamonds = memo(function TimelineClipDiamonds({
|
|
|
135
121
|
const baseOpacity = isSelected ? 0.4 : 0.25;
|
|
136
122
|
const canDrag = isSelected && !!onMoveKeyframe;
|
|
137
123
|
|
|
138
|
-
// Snap a dragged keyframe's clip-% to the nearest beat (within ~8px), then
|
|
139
|
-
// re-clamp to neighbours so the snap can't cross a sibling keyframe. No-op
|
|
140
|
-
// when the beat strip isn't active for this track or no beats are loaded.
|
|
141
|
-
const snapClipPctToBeat = (clipPct: number, draggedIndex: number): number => {
|
|
142
|
-
if (!beatsActive || !beatTimes || beatTimes.length === 0) return clipPct;
|
|
143
|
-
const snapped = snapKeyframePctToBeat(
|
|
144
|
-
{ start: clipStart, duration: clipDurationSeconds },
|
|
145
|
-
clipPct,
|
|
146
|
-
beatTimes,
|
|
147
|
-
pixelsPerSecond,
|
|
148
|
-
);
|
|
149
|
-
return clampToNeighbors(snapped, sortedClipPcts, draggedIndex);
|
|
150
|
-
};
|
|
151
|
-
|
|
152
124
|
return (
|
|
153
125
|
<div
|
|
154
126
|
className="absolute inset-0"
|
|
@@ -223,17 +195,14 @@ export const TimelineClipDiamonds = memo(function TimelineClipDiamonds({
|
|
|
223
195
|
if (d.moved) {
|
|
224
196
|
setPreview({
|
|
225
197
|
kfKey,
|
|
226
|
-
clipPct:
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}),
|
|
235
|
-
i,
|
|
236
|
-
),
|
|
198
|
+
clipPct: previewClipPct({
|
|
199
|
+
pointerDownX: d.startX,
|
|
200
|
+
pointerMoveX: e.clientX,
|
|
201
|
+
clipWidthPx,
|
|
202
|
+
draggedClipPct: d.fromClipPct,
|
|
203
|
+
draggedIndex: i,
|
|
204
|
+
sortedClipPcts,
|
|
205
|
+
}),
|
|
237
206
|
});
|
|
238
207
|
}
|
|
239
208
|
};
|
|
@@ -269,7 +238,7 @@ export const TimelineClipDiamonds = memo(function TimelineClipDiamonds({
|
|
|
269
238
|
if (e.shiftKey) onShiftClickKeyframe?.(elementId, kf.percentage);
|
|
270
239
|
else onClickKeyframe?.(kf.percentage);
|
|
271
240
|
} else if (res.kind === "move" && res.toClipPct != null) {
|
|
272
|
-
onMoveKeyframe?.(elementId, d.fromClipPct,
|
|
241
|
+
onMoveKeyframe?.(elementId, d.fromClipPct, res.toClipPct);
|
|
273
242
|
// A retime still targeted this exact diamond — park/select it at its
|
|
274
243
|
// new position, same as a plain click, or a drag that actually moved
|
|
275
244
|
// something looks identical to one that silently did nothing.
|
|
@@ -37,7 +37,9 @@ export function TimelineEmptyState({
|
|
|
37
37
|
style={{ position: "absolute", left: GUTTER + s * 14 }}
|
|
38
38
|
>
|
|
39
39
|
<span className="text-[9px] text-neutral-600 font-mono tabular-nums leading-none mb-0.5">
|
|
40
|
-
{`${Math.floor(s / 60)
|
|
40
|
+
{`${Math.floor(s / 60)
|
|
41
|
+
.toString()
|
|
42
|
+
.padStart(2, "0")}:${(s % 60).toString().padStart(2, "0")}`}
|
|
41
43
|
</span>
|
|
42
44
|
<div className="w-px h-[5px] bg-neutral-700/40" />
|
|
43
45
|
</div>
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { Eye, EyeSlash } from "@phosphor-icons/react";
|
|
3
|
+
import { BeatStrip, BeatBackgroundLines } from "./BeatStrip";
|
|
4
|
+
import { TimelineClip } from "./TimelineClip";
|
|
5
|
+
import { TimelineClipDiamonds } from "./TimelineClipDiamonds";
|
|
6
|
+
import type { MusicBeatAnalysis } from "@hyperframes/core/beats";
|
|
7
|
+
import { getTimelineEditCapabilities, resolveBlockedTimelineEditIntent } from "./timelineEditing";
|
|
8
|
+
import type { TimelineTheme } from "./timelineTheme";
|
|
9
|
+
import { GUTTER, TRACK_H, CLIP_Y, CLIP_HANDLE_W } from "./timelineLayout";
|
|
10
|
+
import {
|
|
11
|
+
usePlayerStore,
|
|
12
|
+
type TimelineElement,
|
|
13
|
+
type KeyframeCacheEntry,
|
|
14
|
+
} from "../store/playerStore";
|
|
15
|
+
import type { DraggedClipState, ResizingClipState, BlockedClipState } from "./useTimelineClipDrag";
|
|
16
|
+
import {
|
|
17
|
+
isMultiDragPassenger,
|
|
18
|
+
multiDragPassengerOffsetPx,
|
|
19
|
+
type MultiDragPreviewInput,
|
|
20
|
+
} from "./timelineMultiDragPreview";
|
|
21
|
+
import type { TrackVisualStyle } from "./timelineIcons";
|
|
22
|
+
import type { TimelineEditCallbacks } from "./timelineCallbacks";
|
|
23
|
+
import { STUDIO_KEYFRAMES_ENABLED } from "../../components/editor/manualEditingAvailability";
|
|
24
|
+
import { SPLIT_BOUNDARY_EPSILON_S } from "../../utils/timelineElementSplit";
|
|
25
|
+
import { isAudioTimelineElement, isMusicTrack } from "../../utils/timelineInspector";
|
|
26
|
+
import { Music } from "../../icons/SystemIcons";
|
|
27
|
+
import { renderClipChildren } from "./timelineClipChildren";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Props shared by the scroll container ({@link TimelineCanvas}) and the lane
|
|
31
|
+
* renderer below. TimelineCanvas passes these straight through via spread, so
|
|
32
|
+
* they are declared once here and both prop types compose from this base — no
|
|
33
|
+
* duplicated prop list.
|
|
34
|
+
*/
|
|
35
|
+
export interface TimelineLaneBaseProps {
|
|
36
|
+
pps: number;
|
|
37
|
+
trackContentWidth: number;
|
|
38
|
+
theme: TimelineTheme;
|
|
39
|
+
displayTrackOrder: number[];
|
|
40
|
+
trackOrder: number[];
|
|
41
|
+
tracks: [number, TimelineElement[]][];
|
|
42
|
+
trackStyles: Map<number, TrackVisualStyle>;
|
|
43
|
+
selectedElementId: string | null;
|
|
44
|
+
selectedElementIds: Set<string>;
|
|
45
|
+
hoveredClip: string | null;
|
|
46
|
+
draggedClip: DraggedClipState | null;
|
|
47
|
+
blockedClipRef: React.RefObject<BlockedClipState | null>;
|
|
48
|
+
suppressClickRef: React.RefObject<boolean>;
|
|
49
|
+
scrollRef: React.RefObject<HTMLDivElement | null>;
|
|
50
|
+
renderClipContent?: (
|
|
51
|
+
element: TimelineElement,
|
|
52
|
+
style: { clip: string; label: string },
|
|
53
|
+
) => ReactNode;
|
|
54
|
+
renderClipOverlay?: (element: TimelineElement) => ReactNode;
|
|
55
|
+
onDrillDown?: (element: TimelineElement) => void;
|
|
56
|
+
onSelectElement?: (element: TimelineElement | null) => void;
|
|
57
|
+
setHoveredClip: (key: string | null) => void;
|
|
58
|
+
setShowPopover: (v: boolean) => void;
|
|
59
|
+
setRangeSelection: (v: null) => void;
|
|
60
|
+
setResizingClip: (v: ResizingClipState | null) => void;
|
|
61
|
+
setDraggedClip: (v: DraggedClipState | null) => void;
|
|
62
|
+
setSelectedElementId: (id: string | null) => void;
|
|
63
|
+
syncClipDragAutoScroll: (x: number, y: number) => void;
|
|
64
|
+
shiftClickClipRef: React.RefObject<{
|
|
65
|
+
element: TimelineElement;
|
|
66
|
+
anchorX: number;
|
|
67
|
+
anchorY: number;
|
|
68
|
+
} | null>;
|
|
69
|
+
getPreviewElement: (element: TimelineElement) => TimelineElement;
|
|
70
|
+
getTrackStyle: (tag: string) => TrackVisualStyle;
|
|
71
|
+
keyframeCache?: Map<string, KeyframeCacheEntry>;
|
|
72
|
+
selectedKeyframes: Set<string>;
|
|
73
|
+
currentTime: number;
|
|
74
|
+
onClickKeyframe?: (element: TimelineElement, percentage: number) => void;
|
|
75
|
+
onShiftClickKeyframe?: (elementId: string, percentage: number) => void;
|
|
76
|
+
onContextMenuKeyframe?: (e: React.MouseEvent, elementId: string, percentage: number) => void;
|
|
77
|
+
onMoveKeyframe?: (
|
|
78
|
+
elementId: string,
|
|
79
|
+
fromClipPercentage: number,
|
|
80
|
+
toClipPercentage: number,
|
|
81
|
+
) => void;
|
|
82
|
+
onContextMenuClip?: (e: React.MouseEvent, element: TimelineElement) => void;
|
|
83
|
+
beatAnalysis?: MusicBeatAnalysis | null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface TimelineLanesProps extends TimelineLaneBaseProps {
|
|
87
|
+
/** Live-derived by TimelineCanvas from {@link TimelineLaneBaseProps.draggedClip}. */
|
|
88
|
+
draggedElement: TimelineElement | null;
|
|
89
|
+
multiDragPreview: MultiDragPreviewInput | null;
|
|
90
|
+
onToggleTrackHidden: TimelineEditCallbacks["onToggleTrackHidden"];
|
|
91
|
+
onResizeElement: TimelineEditCallbacks["onResizeElement"];
|
|
92
|
+
onMoveElement: TimelineEditCallbacks["onMoveElement"];
|
|
93
|
+
onRazorSplit: TimelineEditCallbacks["onRazorSplit"];
|
|
94
|
+
onRazorSplitAll: TimelineEditCallbacks["onRazorSplitAll"];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function TimelineLanes({
|
|
98
|
+
pps,
|
|
99
|
+
trackContentWidth,
|
|
100
|
+
theme,
|
|
101
|
+
displayTrackOrder,
|
|
102
|
+
trackOrder,
|
|
103
|
+
tracks,
|
|
104
|
+
trackStyles,
|
|
105
|
+
selectedElementId,
|
|
106
|
+
selectedElementIds,
|
|
107
|
+
hoveredClip,
|
|
108
|
+
draggedClip,
|
|
109
|
+
draggedElement,
|
|
110
|
+
multiDragPreview,
|
|
111
|
+
blockedClipRef,
|
|
112
|
+
suppressClickRef,
|
|
113
|
+
scrollRef,
|
|
114
|
+
renderClipContent,
|
|
115
|
+
renderClipOverlay,
|
|
116
|
+
onDrillDown,
|
|
117
|
+
onSelectElement,
|
|
118
|
+
setHoveredClip,
|
|
119
|
+
setShowPopover,
|
|
120
|
+
setRangeSelection,
|
|
121
|
+
setResizingClip,
|
|
122
|
+
setDraggedClip,
|
|
123
|
+
setSelectedElementId,
|
|
124
|
+
syncClipDragAutoScroll,
|
|
125
|
+
shiftClickClipRef,
|
|
126
|
+
getPreviewElement,
|
|
127
|
+
getTrackStyle,
|
|
128
|
+
keyframeCache,
|
|
129
|
+
selectedKeyframes,
|
|
130
|
+
currentTime,
|
|
131
|
+
onClickKeyframe,
|
|
132
|
+
onShiftClickKeyframe,
|
|
133
|
+
onContextMenuKeyframe,
|
|
134
|
+
onMoveKeyframe,
|
|
135
|
+
onContextMenuClip,
|
|
136
|
+
beatAnalysis,
|
|
137
|
+
onToggleTrackHidden,
|
|
138
|
+
onResizeElement,
|
|
139
|
+
onMoveElement,
|
|
140
|
+
onRazorSplit,
|
|
141
|
+
onRazorSplitAll,
|
|
142
|
+
}: TimelineLanesProps) {
|
|
143
|
+
return (
|
|
144
|
+
<>
|
|
145
|
+
{
|
|
146
|
+
// NOTE (deliberate no-virtualization): lanes and their clips render via a
|
|
147
|
+
// plain `.map()` inside the scroll container rather than a windowing/virtualized
|
|
148
|
+
// list. NLE clip counts are small (dozens to low hundreds), so the DOM cost is
|
|
149
|
+
// bounded and virtualization's complexity isn't worth it. TODO: revisit and swap
|
|
150
|
+
// in a virtualizer if editorial workflows ever push very high clip counts.
|
|
151
|
+
// fallow-ignore-next-line complexity
|
|
152
|
+
displayTrackOrder.map((trackNum) => {
|
|
153
|
+
const els = tracks.find(([t]) => t === trackNum)?.[1] ?? [];
|
|
154
|
+
const ts = trackStyles.get(trackNum) ?? getTrackStyle("");
|
|
155
|
+
const isPendingTrack =
|
|
156
|
+
draggedClip?.started === true && !trackOrder.includes(trackNum) && els.length === 0;
|
|
157
|
+
// All lanes use the same uniform color — no alternating stripes.
|
|
158
|
+
const rowBackground = theme.rowBackground;
|
|
159
|
+
// The beat-dot strip occupies the top of this track's lane (active track,
|
|
160
|
+
// or the music track when nothing is selected). When shown, keyframe
|
|
161
|
+
// diamonds shrink + drop to the bottom half so they don't collide with it.
|
|
162
|
+
const beatStripOnTrack =
|
|
163
|
+
(beatAnalysis?.beatTimes?.length ?? 0) >= 2 &&
|
|
164
|
+
(selectedElementId
|
|
165
|
+
? els.some((e) => (e.key ?? e.id) === selectedElementId)
|
|
166
|
+
: els.some(isMusicTrack));
|
|
167
|
+
const isTrackHidden = els.length > 0 && els.every((element) => element.hidden === true);
|
|
168
|
+
const isAudioTrack = els.length > 0 && els.some(isAudioTimelineElement);
|
|
169
|
+
return (
|
|
170
|
+
<div
|
|
171
|
+
key={trackNum}
|
|
172
|
+
className="relative flex"
|
|
173
|
+
style={{
|
|
174
|
+
height: TRACK_H,
|
|
175
|
+
background: rowBackground,
|
|
176
|
+
borderBottom: `1px solid ${theme.rowBorder}`,
|
|
177
|
+
}}
|
|
178
|
+
>
|
|
179
|
+
<div
|
|
180
|
+
className="sticky left-0 z-[12] flex-shrink-0 flex flex-col items-center justify-center gap-0.5"
|
|
181
|
+
style={{
|
|
182
|
+
width: GUTTER,
|
|
183
|
+
background: theme.gutterBackground,
|
|
184
|
+
borderRight: `1px solid ${theme.gutterBorder}`,
|
|
185
|
+
}}
|
|
186
|
+
>
|
|
187
|
+
{isAudioTrack && (
|
|
188
|
+
<Music size={12} weight="fill" aria-hidden="true" className="text-white/35" />
|
|
189
|
+
)}
|
|
190
|
+
<button
|
|
191
|
+
type="button"
|
|
192
|
+
aria-label={isTrackHidden ? `Show track ${trackNum}` : `Hide track ${trackNum}`}
|
|
193
|
+
title={isTrackHidden ? `Show track ${trackNum}` : `Hide track ${trackNum}`}
|
|
194
|
+
className={`flex h-6 w-6 items-center justify-center rounded border-0 bg-transparent p-0 transition-colors focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-[-1px] focus-visible:outline-[#3CE6AC] ${
|
|
195
|
+
isTrackHidden
|
|
196
|
+
? "text-[#3CE6AC] hover:text-white"
|
|
197
|
+
: "text-white/35 hover:text-white/75"
|
|
198
|
+
}`}
|
|
199
|
+
onPointerDown={(e) => {
|
|
200
|
+
e.stopPropagation();
|
|
201
|
+
}}
|
|
202
|
+
onClick={(e) => {
|
|
203
|
+
e.stopPropagation();
|
|
204
|
+
void onToggleTrackHidden?.(trackNum, !isTrackHidden);
|
|
205
|
+
}}
|
|
206
|
+
>
|
|
207
|
+
{isTrackHidden ? (
|
|
208
|
+
<EyeSlash size={14} weight="bold" aria-hidden="true" />
|
|
209
|
+
) : (
|
|
210
|
+
<Eye size={14} weight="bold" aria-hidden="true" />
|
|
211
|
+
)}
|
|
212
|
+
</button>
|
|
213
|
+
</div>
|
|
214
|
+
<div
|
|
215
|
+
style={{
|
|
216
|
+
width: trackContentWidth,
|
|
217
|
+
opacity: isTrackHidden ? 0.35 : 1,
|
|
218
|
+
transition: "opacity 120ms ease",
|
|
219
|
+
}}
|
|
220
|
+
className="relative"
|
|
221
|
+
>
|
|
222
|
+
{/* Faint beat lines in every track's background (behind the clips);
|
|
223
|
+
the active move-snap target is highlighted. */}
|
|
224
|
+
<BeatBackgroundLines
|
|
225
|
+
beatTimes={beatAnalysis?.beatTimes}
|
|
226
|
+
beatStrengths={beatAnalysis?.beatStrengths}
|
|
227
|
+
pps={pps}
|
|
228
|
+
highlightTime={
|
|
229
|
+
draggedClip?.started && draggedClip.snapType === "beat"
|
|
230
|
+
? draggedClip.snapTime
|
|
231
|
+
: null
|
|
232
|
+
}
|
|
233
|
+
/>
|
|
234
|
+
{/* Beat dots on the active track (the one holding the selection),
|
|
235
|
+
falling back to the music track when nothing is selected. */}
|
|
236
|
+
{beatStripOnTrack && (
|
|
237
|
+
<BeatStrip
|
|
238
|
+
beatTimes={beatAnalysis?.beatTimes}
|
|
239
|
+
beatStrengths={beatAnalysis?.beatStrengths}
|
|
240
|
+
pps={pps}
|
|
241
|
+
/>
|
|
242
|
+
)}
|
|
243
|
+
{isPendingTrack && (
|
|
244
|
+
<div
|
|
245
|
+
className="absolute inset-0 flex items-center"
|
|
246
|
+
style={{
|
|
247
|
+
paddingLeft: 16,
|
|
248
|
+
color: ts.label,
|
|
249
|
+
fontSize: 11,
|
|
250
|
+
letterSpacing: "0.06em",
|
|
251
|
+
textTransform: "uppercase",
|
|
252
|
+
opacity: 0.5,
|
|
253
|
+
}}
|
|
254
|
+
>
|
|
255
|
+
New track
|
|
256
|
+
</div>
|
|
257
|
+
)}
|
|
258
|
+
{
|
|
259
|
+
// fallow-ignore-next-line complexity
|
|
260
|
+
els.map((el) => {
|
|
261
|
+
const clipStyle = getTrackStyle(el.tag);
|
|
262
|
+
const elementKey = el.key ?? el.id;
|
|
263
|
+
const capabilities = getTimelineEditCapabilities(el);
|
|
264
|
+
const isSelected =
|
|
265
|
+
selectedElementId === elementKey || selectedElementIds.has(elementKey);
|
|
266
|
+
const isComposition = !!el.compositionSrc;
|
|
267
|
+
// elementKey (el.key ?? el.id) is already unique per clip; do NOT
|
|
268
|
+
// fold in the map index, or a splice/reorder remounts every clip
|
|
269
|
+
// at/after the change (DOM flash, drag interruption).
|
|
270
|
+
const clipKey = elementKey;
|
|
271
|
+
const isDraggingClip =
|
|
272
|
+
draggedClip?.started === true &&
|
|
273
|
+
(draggedElement?.key ?? draggedElement?.id) === elementKey;
|
|
274
|
+
if (isDraggingClip) return null;
|
|
275
|
+
const previewElement = getPreviewElement(el);
|
|
276
|
+
// Passenger of a live multi-drag: slide by the SAME formation
|
|
277
|
+
// delta (the grabbed clip's group-clamped delta) via a
|
|
278
|
+
// compositor transform on a same-geometry wrapper (absolute
|
|
279
|
+
// inset-0 → identical offset parent, so the clip's own
|
|
280
|
+
// left/top are preserved), plus the ghost's elevated z/opacity.
|
|
281
|
+
const isPassenger =
|
|
282
|
+
multiDragPreview != null && isMultiDragPassenger(clipKey, multiDragPreview);
|
|
283
|
+
const passengerOffsetPx = isPassenger
|
|
284
|
+
? multiDragPassengerOffsetPx(clipKey, pps, multiDragPreview)
|
|
285
|
+
: 0;
|
|
286
|
+
const clip = (
|
|
287
|
+
<TimelineClip
|
|
288
|
+
key={clipKey}
|
|
289
|
+
onContextMenu={(e: React.MouseEvent) => {
|
|
290
|
+
e.preventDefault();
|
|
291
|
+
onContextMenuClip?.(e, el);
|
|
292
|
+
}}
|
|
293
|
+
el={previewElement}
|
|
294
|
+
pps={pps}
|
|
295
|
+
clipY={CLIP_Y}
|
|
296
|
+
isSelected={isSelected}
|
|
297
|
+
isHovered={hoveredClip === clipKey}
|
|
298
|
+
isDragging={false}
|
|
299
|
+
hasCustomContent={!!renderClipContent}
|
|
300
|
+
capabilities={capabilities}
|
|
301
|
+
theme={theme}
|
|
302
|
+
isComposition={isComposition}
|
|
303
|
+
onHoverStart={() => setHoveredClip(clipKey)}
|
|
304
|
+
onHoverEnd={() => setHoveredClip(null)}
|
|
305
|
+
onResizeStart={
|
|
306
|
+
// fallow-ignore-next-line complexity
|
|
307
|
+
(edge, e) => {
|
|
308
|
+
if (e.button !== 0 || e.shiftKey || !onResizeElement) return;
|
|
309
|
+
if (edge === "start" && !capabilities.canTrimStart) return;
|
|
310
|
+
if (edge === "end" && !capabilities.canTrimEnd) return;
|
|
311
|
+
e.stopPropagation();
|
|
312
|
+
blockedClipRef.current = null;
|
|
313
|
+
setShowPopover(false);
|
|
314
|
+
setRangeSelection(null);
|
|
315
|
+
setResizingClip({
|
|
316
|
+
element: el,
|
|
317
|
+
edge,
|
|
318
|
+
originClientX: e.clientX,
|
|
319
|
+
originScrollLeft: scrollRef.current?.scrollLeft ?? 0,
|
|
320
|
+
previewStart: el.start,
|
|
321
|
+
previewDuration: el.duration,
|
|
322
|
+
previewPlaybackStart: el.playbackStart,
|
|
323
|
+
started: false,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
onPointerDown={
|
|
328
|
+
// fallow-ignore-next-line complexity
|
|
329
|
+
(e) => {
|
|
330
|
+
if (e.button !== 0) return;
|
|
331
|
+
if (usePlayerStore.getState().activeTool === "razor") return;
|
|
332
|
+
if (e.shiftKey) {
|
|
333
|
+
shiftClickClipRef.current = {
|
|
334
|
+
element: el,
|
|
335
|
+
anchorX: e.clientX,
|
|
336
|
+
anchorY: e.clientY,
|
|
337
|
+
};
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const target = e.currentTarget as HTMLElement;
|
|
341
|
+
const rect = target.getBoundingClientRect();
|
|
342
|
+
const blockedIntent = resolveBlockedTimelineEditIntent({
|
|
343
|
+
width: rect.width,
|
|
344
|
+
offsetX: e.clientX - rect.left,
|
|
345
|
+
handleWidth: CLIP_HANDLE_W,
|
|
346
|
+
capabilities,
|
|
347
|
+
});
|
|
348
|
+
if (
|
|
349
|
+
blockedIntent &&
|
|
350
|
+
((blockedIntent === "move" && onMoveElement) ||
|
|
351
|
+
(blockedIntent !== "move" && onResizeElement))
|
|
352
|
+
) {
|
|
353
|
+
blockedClipRef.current = {
|
|
354
|
+
element: el,
|
|
355
|
+
intent: blockedIntent,
|
|
356
|
+
originClientX: e.clientX,
|
|
357
|
+
originClientY: e.clientY,
|
|
358
|
+
started: false,
|
|
359
|
+
};
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (!onMoveElement || !capabilities.canMove) return;
|
|
363
|
+
blockedClipRef.current = null;
|
|
364
|
+
setShowPopover(false);
|
|
365
|
+
setRangeSelection(null);
|
|
366
|
+
setDraggedClip({
|
|
367
|
+
element: el,
|
|
368
|
+
originClientX: e.clientX,
|
|
369
|
+
originClientY: e.clientY,
|
|
370
|
+
originScrollLeft: scrollRef.current?.scrollLeft ?? 0,
|
|
371
|
+
originScrollTop: scrollRef.current?.scrollTop ?? 0,
|
|
372
|
+
pointerClientX: e.clientX,
|
|
373
|
+
pointerClientY: e.clientY,
|
|
374
|
+
pointerOffsetX: e.clientX - rect.left,
|
|
375
|
+
pointerOffsetY: e.clientY - rect.top,
|
|
376
|
+
previewStart: el.start,
|
|
377
|
+
previewTrack: el.track,
|
|
378
|
+
desiredTrack: el.track,
|
|
379
|
+
insertRow: null,
|
|
380
|
+
snapTime: null,
|
|
381
|
+
snapType: null,
|
|
382
|
+
started: false,
|
|
383
|
+
});
|
|
384
|
+
syncClipDragAutoScroll(e.clientX, e.clientY);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
onClick={
|
|
388
|
+
// fallow-ignore-next-line complexity
|
|
389
|
+
(e) => {
|
|
390
|
+
e.stopPropagation();
|
|
391
|
+
if (suppressClickRef.current) return;
|
|
392
|
+
const { activeTool } = usePlayerStore.getState();
|
|
393
|
+
if (activeTool === "razor" && onRazorSplit) {
|
|
394
|
+
const clipRect = (
|
|
395
|
+
e.currentTarget as HTMLElement
|
|
396
|
+
).getBoundingClientRect();
|
|
397
|
+
const clickOffsetX = e.clientX - clipRect.left;
|
|
398
|
+
const splitTime = previewElement.start + clickOffsetX / pps;
|
|
399
|
+
const clampedTime = Math.max(
|
|
400
|
+
previewElement.start + SPLIT_BOUNDARY_EPSILON_S,
|
|
401
|
+
Math.min(
|
|
402
|
+
previewElement.start +
|
|
403
|
+
previewElement.duration -
|
|
404
|
+
SPLIT_BOUNDARY_EPSILON_S,
|
|
405
|
+
splitTime,
|
|
406
|
+
),
|
|
407
|
+
);
|
|
408
|
+
if (e.shiftKey && onRazorSplitAll) {
|
|
409
|
+
onRazorSplitAll(clampedTime);
|
|
410
|
+
} else {
|
|
411
|
+
onRazorSplit(el, clampedTime);
|
|
412
|
+
}
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
// Plain click single-selects: drop any marquee multi-selection.
|
|
416
|
+
// Only a click on the PRIMARY selection toggles it off — a click
|
|
417
|
+
// on a marquee-selected clip narrows the selection to that clip.
|
|
418
|
+
const hadMultiSelection = selectedElementIds.size > 0;
|
|
419
|
+
usePlayerStore.getState().clearSelectedElementIds();
|
|
420
|
+
const nextElement =
|
|
421
|
+
selectedElementId === elementKey && !hadMultiSelection ? null : el;
|
|
422
|
+
setSelectedElementId(nextElement ? elementKey : null);
|
|
423
|
+
onSelectElement?.(nextElement);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
onDoubleClick={(e) => {
|
|
427
|
+
e.stopPropagation();
|
|
428
|
+
if (suppressClickRef.current) return;
|
|
429
|
+
if (isComposition && onDrillDown) onDrillDown(el);
|
|
430
|
+
}}
|
|
431
|
+
>
|
|
432
|
+
{renderClipChildren(
|
|
433
|
+
previewElement,
|
|
434
|
+
clipStyle,
|
|
435
|
+
renderClipContent,
|
|
436
|
+
renderClipOverlay,
|
|
437
|
+
)}
|
|
438
|
+
{STUDIO_KEYFRAMES_ENABLED && keyframeCache?.get(elementKey) && (
|
|
439
|
+
<TimelineClipDiamonds
|
|
440
|
+
keyframesData={keyframeCache.get(elementKey)!}
|
|
441
|
+
clipWidthPx={Math.max(previewElement.duration * pps, 4)}
|
|
442
|
+
clipHeightPx={TRACK_H - 2 * CLIP_Y}
|
|
443
|
+
beatsActive={beatStripOnTrack}
|
|
444
|
+
accentColor={clipStyle.accent}
|
|
445
|
+
isSelected={isSelected}
|
|
446
|
+
currentPercentage={
|
|
447
|
+
previewElement.duration > 0
|
|
448
|
+
? ((currentTime - previewElement.start) / previewElement.duration) *
|
|
449
|
+
100
|
|
450
|
+
: 0
|
|
451
|
+
}
|
|
452
|
+
elementId={elementKey}
|
|
453
|
+
selectedKeyframes={selectedKeyframes}
|
|
454
|
+
onClickKeyframe={(pct) => onClickKeyframe?.(previewElement, pct)}
|
|
455
|
+
onShiftClickKeyframe={onShiftClickKeyframe}
|
|
456
|
+
onContextMenuKeyframe={onContextMenuKeyframe}
|
|
457
|
+
onMoveKeyframe={onMoveKeyframe}
|
|
458
|
+
suppressClickRef={suppressClickRef}
|
|
459
|
+
/>
|
|
460
|
+
)}
|
|
461
|
+
</TimelineClip>
|
|
462
|
+
);
|
|
463
|
+
if (!isPassenger) return clip;
|
|
464
|
+
return (
|
|
465
|
+
<div
|
|
466
|
+
key={clipKey}
|
|
467
|
+
className="absolute inset-0"
|
|
468
|
+
style={{
|
|
469
|
+
transform: `translateX(${passengerOffsetPx}px)`,
|
|
470
|
+
opacity: 0.85,
|
|
471
|
+
zIndex: 20,
|
|
472
|
+
pointerEvents: "none",
|
|
473
|
+
}}
|
|
474
|
+
>
|
|
475
|
+
{clip}
|
|
476
|
+
</div>
|
|
477
|
+
);
|
|
478
|
+
})
|
|
479
|
+
}
|
|
480
|
+
</div>
|
|
481
|
+
</div>
|
|
482
|
+
);
|
|
483
|
+
})
|
|
484
|
+
}
|
|
485
|
+
</>
|
|
486
|
+
);
|
|
487
|
+
}
|