@hyperframes/studio 0.7.55 → 0.7.57

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.
Files changed (274) hide show
  1. package/dist/assets/hyperframes-player--Z69cEkE.js +459 -0
  2. package/dist/assets/{index-CPetwHFV.js → index-Bf1x1y8H.js} +1 -1
  3. package/dist/assets/index-DfmYkU44.js +423 -0
  4. package/dist/assets/{index-BXaqaVKt.js → index-KsfE1bUu.js} +1 -1
  5. package/dist/assets/index-_pqzyxB1.css +1 -0
  6. package/dist/{chunk-SOTCF4DF.js → chunk-5QSIMBEJ.js} +2 -1
  7. package/dist/chunk-5QSIMBEJ.js.map +1 -0
  8. package/dist/{domEditingLayers-2ECJK24D.js → domEditingLayers-6LQGKPOI.js} +2 -2
  9. package/dist/index.d.ts +172 -144
  10. package/dist/index.html +2 -2
  11. package/dist/index.js +43613 -40346
  12. package/dist/index.js.map +1 -1
  13. package/package.json +7 -7
  14. package/src/App.tsx +106 -96
  15. package/src/components/EditorShell.tsx +253 -0
  16. package/src/components/StudioGlobalDragOverlay.tsx +1 -3
  17. package/src/components/StudioLeftSidebar.tsx +13 -3
  18. package/src/components/StudioRightPanel.tsx +8 -3
  19. package/src/components/TimelineToolbar.test.tsx +2 -2
  20. package/src/components/TimelineToolbar.tsx +179 -121
  21. package/src/components/editor/CanvasContextMenu.test.tsx +152 -1
  22. package/src/components/editor/CanvasContextMenu.tsx +33 -18
  23. package/src/components/editor/DomEditCropHandles.test.tsx +74 -3
  24. package/src/components/editor/DomEditCropHandles.tsx +64 -29
  25. package/src/components/editor/DomEditOverlay.test.ts +245 -279
  26. package/src/components/editor/DomEditOverlay.tsx +126 -125
  27. package/src/components/editor/DomEditOverlayHover.test.tsx +90 -0
  28. package/src/components/editor/DomEditRotateHandle.tsx +38 -18
  29. package/src/components/editor/DomEditSelectionChrome.test.tsx +80 -0
  30. package/src/components/editor/DomEditSelectionChrome.tsx +257 -0
  31. package/src/components/editor/LayersPanel.test.ts +66 -2
  32. package/src/components/editor/LayersPanel.tsx +53 -2
  33. package/src/components/editor/OffCanvasIndicators.test.tsx +34 -0
  34. package/src/components/editor/OffCanvasIndicators.tsx +49 -10
  35. package/src/components/editor/SnapGuideOverlay.tsx +28 -23
  36. package/src/components/editor/SnapToolbar.test.tsx +0 -1
  37. package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +214 -0
  38. package/src/components/editor/anchoredResizeReleaseShift.test.ts +106 -10
  39. package/src/components/editor/canvasContextMenuZOrder.test.ts +186 -2
  40. package/src/components/editor/canvasContextMenuZOrder.ts +144 -22
  41. package/src/components/editor/domEditNudge.test.ts +80 -0
  42. package/src/components/editor/domEditNudge.ts +44 -0
  43. package/src/components/editor/domEditOverlayGeometry.test.ts +167 -1
  44. package/src/components/editor/domEditOverlayGeometry.ts +280 -20
  45. package/src/components/editor/domEditOverlayGestures.ts +44 -41
  46. package/src/components/editor/domEditOverlayStartGesture.ts +62 -2
  47. package/src/components/editor/domEditResizeLocal.test.ts +131 -0
  48. package/src/components/editor/domEditResizeLocal.ts +125 -0
  49. package/src/components/editor/domEditingDom.ts +1 -1
  50. package/src/components/editor/manualEditsDomPatches.test.ts +47 -0
  51. package/src/components/editor/manualOffsetDrag.ts +55 -12
  52. package/src/components/editor/offCanvasIndicatorGeometry.ts +31 -10
  53. package/src/components/editor/offCanvasIndicatorRefresh.test.tsx +25 -1
  54. package/src/components/editor/propertyPanelColorGradingSection.tsx +15 -21
  55. package/src/components/editor/resizeDraft.ts +108 -0
  56. package/src/components/editor/snapEngine.test.ts +12 -78
  57. package/src/components/editor/snapEngine.ts +13 -53
  58. package/src/components/editor/useCanvasContextMenuState.ts +92 -0
  59. package/src/components/editor/useDomEditNudge.test.tsx +228 -0
  60. package/src/components/editor/useDomEditNudge.ts +254 -0
  61. package/src/components/editor/useDomEditOverlayGestures.ts +115 -116
  62. package/src/components/editor/useDomEditOverlayRects.ts +9 -2
  63. package/src/components/editor/useZOrderCrossedFlash.tsx +68 -0
  64. package/src/components/nle/AssetPreviewOverlay.test.tsx +106 -0
  65. package/src/components/nle/AssetPreviewOverlay.tsx +171 -0
  66. package/src/components/nle/NLEContext.test.ts +144 -0
  67. package/src/components/nle/NLEContext.tsx +328 -0
  68. package/src/components/nle/NLEPreview.tsx +1 -1
  69. package/src/components/nle/PreviewOverlays.tsx +260 -0
  70. package/src/components/nle/PreviewPane.tsx +163 -0
  71. package/src/components/nle/TimelinePane.test.ts +60 -0
  72. package/src/components/nle/TimelinePane.tsx +293 -0
  73. package/src/components/nle/TimelineResizeDivider.tsx +8 -2
  74. package/src/components/nle/useCompositionStack.test.tsx +44 -0
  75. package/src/components/nle/useCompositionStack.ts +17 -9
  76. package/src/components/nle/useTimelineEditCallbacks.ts +217 -0
  77. package/src/components/sidebar/AssetCard.test.tsx +106 -0
  78. package/src/components/sidebar/AssetCard.tsx +343 -0
  79. package/src/components/sidebar/AssetContextMenu.tsx +15 -30
  80. package/src/components/sidebar/AssetsTab.test.ts +96 -0
  81. package/src/components/sidebar/AssetsTab.tsx +71 -206
  82. package/src/components/sidebar/AudioRow.tsx +56 -2
  83. package/src/components/sidebar/BlocksTab.tsx +8 -1
  84. package/src/components/sidebar/LeftSidebar.tsx +4 -1
  85. package/src/components/sidebar/assetHelpers.ts +29 -0
  86. package/src/contexts/DomEditContext.tsx +9 -0
  87. package/src/contexts/StudioContext.tsx +9 -8
  88. package/src/contexts/TimelineEditContext.tsx +3 -7
  89. package/src/hooks/deleteSelectedKeyframes.ts +35 -0
  90. package/src/hooks/domEditCommitTypes.ts +23 -1
  91. package/src/hooks/domSelectionTestHarness.ts +14 -0
  92. package/src/hooks/gestureTransaction.test.ts +311 -0
  93. package/src/hooks/gestureTransaction.ts +199 -0
  94. package/src/hooks/gsapDragCommit.test.ts +141 -41
  95. package/src/hooks/gsapDragCommit.ts +64 -74
  96. package/src/hooks/gsapDragStaticSetHelpers.ts +9 -21
  97. package/src/hooks/gsapResizeIntercept.test.ts +69 -0
  98. package/src/hooks/gsapResizeIntercept.ts +38 -10
  99. package/src/hooks/gsapRuntimeBridge.test.ts +49 -13
  100. package/src/hooks/gsapRuntimeBridge.ts +4 -4
  101. package/src/hooks/gsapScriptCommitTypes.ts +16 -5
  102. package/src/hooks/gsapShared.test.ts +19 -1
  103. package/src/hooks/gsapShared.ts +14 -0
  104. package/src/hooks/patchDocumentRootDuration.test.ts +71 -0
  105. package/src/hooks/timelineEditingGsap.ts +14 -0
  106. package/src/hooks/timelineEditingHelpers.test.ts +125 -1
  107. package/src/hooks/timelineEditingHelpers.ts +53 -292
  108. package/src/hooks/timelineMoveAdapter.test.ts +81 -0
  109. package/src/hooks/timelineMoveAdapter.ts +39 -0
  110. package/src/hooks/timelineTimingSync.test.ts +150 -0
  111. package/src/hooks/timelineTimingSync.ts +412 -0
  112. package/src/hooks/useAnimatedPropertyCommit.test.tsx +36 -8
  113. package/src/hooks/useAnimatedPropertyCommit.ts +31 -26
  114. package/src/hooks/useAppHotkeys.ts +16 -25
  115. package/src/hooks/useBlockHandlers.ts +55 -27
  116. package/src/hooks/useCaptionDetection.ts +2 -0
  117. package/src/hooks/useCompositionDimensions.ts +32 -5
  118. package/src/hooks/useContextMenuDismiss.ts +31 -7
  119. package/src/hooks/useDomEditCommits.test.tsx +245 -1
  120. package/src/hooks/useDomEditCommits.ts +162 -3
  121. package/src/hooks/useDomEditPositionPatchCommit.ts +2 -0
  122. package/src/hooks/useDomEditSession.ts +2 -2
  123. package/src/hooks/useDomEditTextCommits.test.tsx +135 -0
  124. package/src/hooks/useDomEditTextCommits.ts +8 -3
  125. package/src/hooks/useDomEditWiring.ts +7 -9
  126. package/src/hooks/useDomGeometryCommits.ts +17 -2
  127. package/src/hooks/useDomSelection.test.ts +2 -36
  128. package/src/hooks/useDomSelection.ts +81 -51
  129. package/src/hooks/useDomSelectionSelectionGuards.test.ts +276 -0
  130. package/src/hooks/useElementLifecycleOps.test.tsx +423 -0
  131. package/src/hooks/useElementLifecycleOps.ts +53 -71
  132. package/src/hooks/useElementPicker.ts +7 -8
  133. package/src/hooks/useEnableKeyframes.test.ts +142 -1
  134. package/src/hooks/useEnableKeyframes.ts +73 -29
  135. package/src/hooks/useGestureCommit.test.tsx +130 -0
  136. package/src/hooks/useGestureCommit.ts +32 -8
  137. package/src/hooks/useGroupCommits.ts +7 -28
  138. package/src/hooks/useGsapAwareEditing.test.tsx +239 -0
  139. package/src/hooks/useGsapAwareEditing.ts +115 -20
  140. package/src/hooks/useGsapKeyframeOps.test.tsx +59 -0
  141. package/src/hooks/useGsapKeyframeOps.ts +38 -10
  142. package/src/hooks/useGsapScriptCommits.test.tsx +40 -1
  143. package/src/hooks/useGsapScriptCommits.ts +134 -50
  144. package/src/hooks/useGsapSelectionHandlers.ts +49 -16
  145. package/src/hooks/useMusicBeatAnalysis.ts +72 -36
  146. package/src/hooks/usePersistentEditHistory.test.ts +33 -0
  147. package/src/hooks/usePersistentEditHistory.ts +92 -58
  148. package/src/hooks/usePreviewPersistence.ts +25 -7
  149. package/src/hooks/useRazorSplit.history.test.tsx +148 -18
  150. package/src/hooks/useRazorSplit.test.ts +269 -0
  151. package/src/hooks/useRazorSplit.testHelpers.ts +64 -0
  152. package/src/hooks/useRazorSplit.ts +107 -36
  153. package/src/hooks/useRenderClipContent.ts +24 -6
  154. package/src/hooks/useStudioContextValue.ts +15 -5
  155. package/src/hooks/useStudioUrlState.ts +2 -4
  156. package/src/hooks/useTimelineAssetDropOps.ts +175 -0
  157. package/src/hooks/useTimelineEditing.test.tsx +398 -241
  158. package/src/hooks/useTimelineEditing.ts +181 -265
  159. package/src/hooks/useTimelineEditingTypes.ts +6 -0
  160. package/src/hooks/useTimelineGroupEditing.ts +167 -132
  161. package/src/index.ts +1 -1
  162. package/src/player/components/ImageThumbnail.test.tsx +173 -0
  163. package/src/player/components/ImageThumbnail.tsx +160 -0
  164. package/src/player/components/PlayerControls.tsx +5 -5
  165. package/src/player/components/PlayheadIndicator.tsx +55 -5
  166. package/src/player/components/Timeline.test.ts +183 -23
  167. package/src/player/components/Timeline.tsx +194 -194
  168. package/src/player/components/TimelineCanvas.tsx +218 -523
  169. package/src/player/components/TimelineClip.tsx +4 -1
  170. package/src/player/components/TimelineClipDiamonds.tsx +9 -40
  171. package/src/player/components/TimelineEmptyState.tsx +3 -1
  172. package/src/player/components/TimelineLanes.tsx +487 -0
  173. package/src/player/components/TimelineOverlays.tsx +122 -0
  174. package/src/player/components/TimelineRuler.tsx +58 -48
  175. package/src/player/components/VideoThumbnail.test.tsx +152 -0
  176. package/src/player/components/VideoThumbnail.tsx +28 -7
  177. package/src/player/components/thumbnailUtils.test.ts +127 -0
  178. package/src/player/components/thumbnailUtils.ts +54 -0
  179. package/src/player/components/timelineCallbacks.ts +18 -18
  180. package/src/player/components/timelineClipChildren.tsx +39 -0
  181. package/src/player/components/timelineClipDragCommit.test.ts +1026 -0
  182. package/src/player/components/timelineClipDragCommit.ts +498 -0
  183. package/src/player/components/timelineClipDragPreview.test.ts +144 -0
  184. package/src/player/components/timelineClipDragPreview.ts +323 -38
  185. package/src/player/components/timelineClipDragTypes.ts +65 -0
  186. package/src/player/components/timelineCollision.test.ts +42 -2
  187. package/src/player/components/timelineDragDrop.ts +69 -60
  188. package/src/player/components/timelineEditCapabilities.ts +52 -0
  189. package/src/player/components/timelineEditing.test.ts +1 -1
  190. package/src/player/components/timelineEditing.ts +76 -59
  191. package/src/player/components/timelineGroupEditing.test.ts +126 -0
  192. package/src/player/components/timelineGroupEditing.ts +133 -0
  193. package/src/player/components/timelineGroupResizeCommit.ts +54 -0
  194. package/src/player/components/timelineLayout.test.ts +95 -23
  195. package/src/player/components/timelineLayout.ts +207 -75
  196. package/src/player/components/timelineMarquee.test.ts +197 -0
  197. package/src/player/components/timelineMarquee.ts +112 -0
  198. package/src/player/components/timelineOptimisticRevision.ts +42 -0
  199. package/src/player/components/timelineRevealScroll.test.ts +133 -0
  200. package/src/player/components/timelineRevealScroll.ts +91 -0
  201. package/src/player/components/timelineStackingSync.test.ts +60 -0
  202. package/src/player/components/timelineStackingSync.ts +27 -2
  203. package/src/player/components/timelineTheme.ts +6 -1
  204. package/src/player/components/timelineTrackPersistPipeline.test.ts +168 -0
  205. package/src/player/components/timelineZones.test.ts +122 -310
  206. package/src/player/components/timelineZones.ts +82 -135
  207. package/src/player/components/useResolvedTimelineEditCallbacks.ts +25 -3
  208. package/src/player/components/useTimelineClipDrag.resize.test.tsx +241 -0
  209. package/src/player/components/useTimelineClipDrag.ts +380 -386
  210. package/src/player/components/useTimelineEditPinning.ts +121 -0
  211. package/src/player/components/useTimelineGeometry.ts +129 -0
  212. package/src/player/components/useTimelinePlayhead.ts +4 -16
  213. package/src/player/components/useTimelineRangeSelection.ts +341 -50
  214. package/src/player/components/useTimelineRevealClip.ts +56 -0
  215. package/src/player/components/useTimelineStackingSync.test.tsx +77 -0
  216. package/src/player/components/useTimelineStackingSync.ts +94 -0
  217. package/src/player/hooks/useExpandedTimelineElements.ts +7 -3
  218. package/src/player/hooks/usePlaybackKeyboard.ts +5 -0
  219. package/src/player/hooks/useTimelinePlayer.seek.test.ts +55 -0
  220. package/src/player/hooks/useTimelinePlayer.test.ts +36 -3
  221. package/src/player/hooks/useTimelinePlayer.ts +52 -53
  222. package/src/player/hooks/useTimelinePlayerLoop.ts +85 -0
  223. package/src/player/hooks/useTimelineSyncCallbacks.ts +9 -1
  224. package/src/player/index.ts +5 -2
  225. package/src/player/lib/runtimeProtocol.test.ts +48 -0
  226. package/src/player/lib/runtimeProtocol.ts +65 -0
  227. package/src/player/lib/time.test.ts +19 -19
  228. package/src/player/lib/time.ts +6 -2
  229. package/src/player/lib/timelineDOM.test.ts +25 -98
  230. package/src/player/lib/timelineDOM.ts +21 -81
  231. package/src/player/lib/timelineElementHelpers.ts +73 -5
  232. package/src/player/lib/timelineIframeHelpers.ts +4 -4
  233. package/src/player/store/playerStore.test.ts +38 -13
  234. package/src/player/store/playerStore.ts +99 -3
  235. package/src/styles/studio.css +27 -0
  236. package/src/utils/assetPreviewDismiss.test.ts +27 -0
  237. package/src/utils/assetPreviewDismiss.ts +29 -0
  238. package/src/utils/assetPreviewStore.ts +33 -0
  239. package/src/utils/blockInstaller.ts +10 -2
  240. package/src/utils/editHistory.test.ts +35 -0
  241. package/src/utils/gsapSoftReload.test.ts +119 -1
  242. package/src/utils/gsapSoftReload.ts +153 -0
  243. package/src/utils/mediaTypes.ts +3 -2
  244. package/src/utils/resizeDebug.ts +55 -0
  245. package/src/utils/rootDuration.test.ts +90 -1
  246. package/src/utils/rootDuration.ts +76 -13
  247. package/src/utils/sdkCutover.ts +4 -0
  248. package/src/utils/studioFileHistory.ts +37 -7
  249. package/src/utils/studioHelpers.ts +62 -0
  250. package/src/utils/studioPreviewHelpers.test.ts +65 -8
  251. package/src/utils/studioPreviewHelpers.ts +10 -0
  252. package/src/utils/studioTelemetry.ts +4 -1
  253. package/src/utils/studioUrlState.test.ts +0 -1
  254. package/src/utils/timelineAssetDrop.test.ts +144 -52
  255. package/src/utils/timelineAssetDrop.ts +67 -29
  256. package/src/utils/timelineDiscovery.ts +0 -17
  257. package/dist/assets/hyperframes-player-BBrKzTGd.js +0 -459
  258. package/dist/assets/index-BRwkMj0w.js +0 -423
  259. package/dist/assets/index-Dq7FEg0K.css +0 -1
  260. package/dist/chunk-SOTCF4DF.js.map +0 -1
  261. package/src/components/StudioPreviewArea.tsx +0 -500
  262. package/src/components/nle/NLELayout.test.ts +0 -12
  263. package/src/components/nle/NLELayout.tsx +0 -591
  264. package/src/player/components/TimelineLayerGutter.tsx +0 -61
  265. package/src/player/components/TimelineSelectionOverlays.tsx +0 -55
  266. package/src/player/components/timelineMarqueeSelection.test.ts +0 -87
  267. package/src/player/components/timelineSnapTargets.test.ts +0 -144
  268. package/src/player/components/timelineSnapTargets.ts +0 -164
  269. package/src/player/components/useTimelineClipDrag.test.tsx +0 -506
  270. package/src/player/components/useTimelineClipGroupDrag.ts +0 -417
  271. package/src/player/components/useTimelineMarqueeSelection.test.tsx +0 -231
  272. package/src/player/components/useTimelineMarqueeSelection.ts +0 -276
  273. package/src/utils/timelineDiscovery.test.ts +0 -90
  274. /package/dist/{domEditingLayers-2ECJK24D.js.map → domEditingLayers-6LQGKPOI.js.map} +0 -0
@@ -1,173 +1,51 @@
1
- import { Fragment, memo, type ReactNode } from "react";
2
- import { BeatStrip, BeatBackgroundLines } from "./BeatStrip";
3
- import { TimelineClip } from "./TimelineClip";
4
- import { TimelineClipDiamonds } from "./TimelineClipDiamonds";
1
+ import { memo } from "react";
5
2
  import { TimelineRuler } from "./TimelineRuler";
6
- import type { MusicBeatAnalysis } from "@hyperframes/core/beats";
7
3
  import { PlayheadIndicator } from "./PlayheadIndicator";
4
+ import { getTimelineEditCapabilities, type TimelineRangeSelection } from "./timelineEditing";
5
+ import { getRenderedTimelineElement } from "./timelineTheme";
8
6
  import {
9
- getTimelineEditCapabilities,
10
- resolveBlockedTimelineEditIntent,
11
- type TimelineRangeSelection,
12
- } from "./timelineEditing";
13
- import { getRenderedTimelineElement, type TimelineTheme } from "./timelineTheme";
14
- import { GUTTER, TRACK_H, CLIP_Y, CLIP_HANDLE_W } from "./timelineLayout";
15
- import {
16
- usePlayerStore,
17
- type TimelineElement,
18
- type KeyframeCacheEntry,
19
- } from "../store/playerStore";
20
- import type { DraggedClipState, ResizingClipState, BlockedClipState } from "./useTimelineClipDrag";
21
- import type { TrackVisualStyle } from "./timelineIcons";
22
- import type { StackingTimelineLayer, TimelineLayerId } from "./timelineTrackOrder";
23
- import { STUDIO_KEYFRAMES_ENABLED } from "../../components/editor/manualEditingAvailability";
24
- import { SPLIT_BOUNDARY_EPSILON_S } from "../../utils/timelineElementSplit";
7
+ GUTTER,
8
+ TRACK_H,
9
+ RULER_H,
10
+ CLIP_Y,
11
+ TRACKS_TOP_PAD,
12
+ TRACKS_BOTTOM_PAD,
13
+ PLAYHEAD_HEAD_W,
14
+ getTimelinePlayheadLeft,
15
+ getTimelineRowTop,
16
+ } from "./timelineLayout";
17
+ import { usePlayerStore } from "../store/playerStore";
18
+ import type { ResizingClipState } from "./useTimelineClipDrag";
19
+ import { type MultiDragPreviewInput } from "./timelineMultiDragPreview";
25
20
  import { useTimelineEditContextOptional } from "../../contexts/TimelineEditContext";
26
- import { isMusicTrack } from "../../utils/timelineInspector";
27
- import { TimelineLayerGutter } from "./TimelineLayerGutter";
28
- import {
29
- shouldShowTimelineLayerGroupHeader,
30
- TimelineLayerGroupHeader,
31
- } from "./TimelineLayerGroupHeader";
32
- import { resolveTimelineDropIndicator } from "./timelineDropIndicator";
33
- import { TimelineDropInsertionLine } from "./TimelineDropInsertionLine";
34
- import { TimelineDragGhost } from "./TimelineDragGhost";
35
- import { TimelineSelectionOverlays } from "./TimelineSelectionOverlays";
36
- import type { TimelineMarqueeOverlayRect } from "./useTimelineMarqueeSelection";
37
-
38
- function ClipLintDot({ element }: { element: TimelineElement }) {
39
- const lint = usePlayerStore((s) => s.lintFindingsByElement.get(element.key ?? element.id));
40
- if (!lint || lint.count === 0) return null;
41
- return (
42
- <span
43
- className="absolute w-1.5 h-1.5 rounded-full bg-amber-400"
44
- style={{ top: 7, right: 7 }}
45
- title={lint.messages.join("\n")}
46
- />
47
- );
48
- }
21
+ import type { Rect } from "../../utils/marqueeGeometry";
22
+ import { TimelineClip } from "./TimelineClip";
23
+ import { TimelineLanes, type TimelineLaneBaseProps } from "./TimelineLanes";
24
+ import { renderClipChildren } from "./timelineClipChildren";
25
+ import { useTimelineRevealClip } from "./useTimelineRevealClip";
49
26
 
50
- interface TimelineCanvasProps {
27
+ interface TimelineCanvasProps extends TimelineLaneBaseProps {
51
28
  major: number[];
52
29
  minor: number[];
53
- pps: number;
54
- trackContentWidth: number;
55
30
  totalH: number;
56
31
  effectiveDuration: number;
57
32
  majorTickInterval: number;
58
33
  rangeSelection: TimelineRangeSelection | null;
59
- marqueeRect: TimelineMarqueeOverlayRect | null;
60
- theme: TimelineTheme;
61
- displayTrackOrder: TimelineLayerId[];
62
- trackOrder: TimelineLayerId[];
63
- tracks: StackingTimelineLayer[];
64
- trackStyles: Map<TimelineLayerId, TrackVisualStyle>;
65
- selectedElementId: string | null;
66
- hoveredClip: string | null;
67
- draggedClip: DraggedClipState | null;
34
+ /** Live rubber-band multi-select rectangle (canvas coordinates), or null. */
35
+ marqueeRect: Rect | null;
68
36
  resizingClip: ResizingClipState | null;
69
- blockedClipRef: React.RefObject<BlockedClipState | null>;
70
- suppressClickRef: React.RefObject<boolean>;
71
- scrollRef: React.RefObject<HTMLDivElement | null>;
72
- renderClipContent?: (
73
- element: TimelineElement,
74
- style: { clip: string; label: string },
75
- ) => ReactNode;
76
- renderClipOverlay?: (element: TimelineElement) => ReactNode;
37
+ /** Playhead is being actively scrubbed — fills the grab-handle head. */
38
+ isScrubbing: boolean;
77
39
  playheadRef: React.RefObject<HTMLDivElement | null>;
78
- onDrillDown?: (element: TimelineElement) => void;
79
- onSelectElement?: (element: TimelineElement | null) => void;
80
- setHoveredClip: (key: string | null) => void;
81
- setShowPopover: (v: boolean) => void;
82
- setRangeSelection: (v: null) => void;
83
- setResizingClip: (v: ResizingClipState | null) => void;
84
- setDraggedClip: (v: DraggedClipState | null) => void;
85
- setSelectedElementId: (id: string | null) => void;
86
- syncClipDragAutoScroll: (x: number, y: number) => void;
87
- shiftClickClipRef: React.RefObject<{
88
- element: TimelineElement;
89
- anchorX: number;
90
- anchorY: number;
91
- } | null>;
92
- getPreviewElement: (element: TimelineElement) => TimelineElement;
93
- getTrackStyle: (tag: string) => TrackVisualStyle;
94
- keyframeCache?: Map<string, KeyframeCacheEntry>;
95
- selectedKeyframes: Set<string>;
96
- currentTime: number;
97
- onClickKeyframe?: (element: TimelineElement, percentage: number) => void;
98
- onShiftClickKeyframe?: (elementId: string, percentage: number) => void;
99
- onContextMenuKeyframe?: (e: React.MouseEvent, elementId: string, percentage: number) => void;
100
- onMoveKeyframe?: (
101
- elementId: string,
102
- fromClipPercentage: number,
103
- toClipPercentage: number,
104
- ) => void;
105
- onContextMenuClip?: (e: React.MouseEvent, element: TimelineElement) => void;
106
- beatAnalysis?: MusicBeatAnalysis | null;
107
40
  }
108
41
 
109
- export const TimelineCanvas = memo(function TimelineCanvas({
110
- major,
111
- minor,
112
- pps,
113
- trackContentWidth,
114
- totalH,
115
- effectiveDuration,
116
- majorTickInterval,
117
- rangeSelection,
118
- marqueeRect,
119
- theme,
120
- displayTrackOrder,
121
- trackOrder,
122
- tracks,
123
- trackStyles,
124
- selectedElementId,
125
- hoveredClip,
126
- draggedClip,
127
- resizingClip,
128
- blockedClipRef,
129
- suppressClickRef,
130
- scrollRef,
131
- renderClipContent,
132
- renderClipOverlay,
133
- playheadRef,
134
- onDrillDown,
135
- onSelectElement,
136
- setHoveredClip,
137
- setShowPopover,
138
- setRangeSelection,
139
- setResizingClip,
140
- setDraggedClip,
141
- setSelectedElementId,
142
- syncClipDragAutoScroll,
143
- shiftClickClipRef,
144
- getPreviewElement,
145
- getTrackStyle,
146
- keyframeCache,
147
- selectedKeyframes,
148
- currentTime,
149
- onClickKeyframe,
150
- onShiftClickKeyframe,
151
- onContextMenuKeyframe,
152
- onMoveKeyframe,
153
- onContextMenuClip,
154
- beatAnalysis,
155
- }: TimelineCanvasProps) {
156
- const {
157
- onResizeElement,
158
- onMoveElement,
159
- onToggleTrackHidden,
160
- onToggleElementHidden,
161
- onRazorSplit,
162
- onRazorSplitAll,
163
- } = useTimelineEditContextOptional();
42
+ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvasProps) {
43
+ const { draggedClip, scrollRef, selectedElementIds, displayTrackOrder } = props;
44
+ const { onResizeElement, onMoveElement, onToggleTrackHidden, onRazorSplit, onRazorSplitAll } =
45
+ useTimelineEditContextOptional();
164
46
  const beatDragging = usePlayerStore((s) => s.beatDragging);
165
- const selectedElementIds = usePlayerStore((s) => s.selectedElementIds);
166
- const activeSnapGuideTime = draggedClip?.started
167
- ? (draggedClip.snapBeatTime ?? draggedClip.snapGuideTime)
168
- : resizingClip?.started
169
- ? resizingClip.snapGuideTime
170
- : null;
47
+ // Scroll a clip into view when the sidebar (asset card) requests a reveal.
48
+ useTimelineRevealClip(scrollRef);
171
49
  const draggedElement = draggedClip?.element ?? null;
172
50
  const activeDraggedElement =
173
51
  draggedClip?.started === true && draggedElement
@@ -178,6 +56,28 @@ export const TimelineCanvas = memo(function TimelineCanvas({
178
56
  previewTrack: draggedClip.previewTrack,
179
57
  })
180
58
  : null;
59
+ // The drag ghost follows the cursor freely (both axes) — CapCut-style. The
60
+ // "magnetic" affordance is a highlight on the destination lane (draggedRowIndex),
61
+ // which flips at the MAGNETIC_TRACK_THRESHOLD point; the clip drops into it.
62
+ const draggedRowIndex =
63
+ draggedClip?.started === true ? displayTrackOrder.indexOf(draggedClip.previewTrack) : -1;
64
+ // Live multi-selection drag: while a selected clip is dragged, ALL selected
65
+ // clips move together as one rigid formation. The GRABBED clip is the free
66
+ // ghost below; its co-selected "passengers" slide by the SAME group-clamped
67
+ // delta (cheap translateX, no re-layout) — the delta is derived from the
68
+ // grabbed clip's ALREADY-clamped previewStart, so the whole formation stops at
69
+ // the wall together and never deforms. Matches what the commit will do — see
70
+ // timelineMultiDragPreview + commit.
71
+ const multiDragPreview: MultiDragPreviewInput | null =
72
+ draggedClip?.started === true && draggedElement
73
+ ? {
74
+ dragStarted: true,
75
+ draggedKey: draggedElement.key ?? draggedElement.id,
76
+ draggedOriginStart: draggedElement.start,
77
+ draggedPreviewStart: draggedClip.previewStart,
78
+ selectedKeys: selectedElementIds,
79
+ }
80
+ : null;
181
81
  const activeDraggedPosition =
182
82
  draggedClip?.started === true && activeDraggedElement && scrollRef.current
183
83
  ? {
@@ -194,396 +94,191 @@ export const TimelineCanvas = memo(function TimelineCanvas({
194
94
  }
195
95
  : null;
196
96
 
197
- const renderClipChildren = (element: TimelineElement, clipStyle: TrackVisualStyle) => (
198
- <>
199
- {renderClipOverlay?.(element)}
200
- {!renderClipContent && <ClipLintDot element={element} />}
201
- {renderClipContent && (
202
- <div className="absolute inset-0 overflow-hidden">
203
- {renderClipContent(element, clipStyle)}
204
- </div>
205
- )}
206
- </>
207
- );
208
- const activeDropPlacement =
209
- draggedClip?.started === true ? (draggedClip.previewStackingReorder?.placement ?? null) : null;
210
-
211
97
  return (
212
- // minWidth:100% makes the lanes and ruler fill the panel when the composition
213
- // is narrower than the viewport (zoomed out); content stays in time coords.
214
98
  <div
215
99
  className="relative"
216
- style={{ height: totalH, width: GUTTER + trackContentWidth, minWidth: "100%" }}
100
+ style={{ height: props.totalH, width: GUTTER + props.trackContentWidth }}
217
101
  >
218
102
  <TimelineRuler
219
- major={major}
220
- minor={minor}
221
- pps={pps}
222
- trackContentWidth={trackContentWidth}
223
- totalH={totalH}
224
- effectiveDuration={effectiveDuration}
225
- majorTickInterval={majorTickInterval}
226
- theme={theme}
227
- beatAnalysis={beatAnalysis}
103
+ major={props.major}
104
+ minor={props.minor}
105
+ pps={props.pps}
106
+ trackContentWidth={props.trackContentWidth}
107
+ totalH={props.totalH}
108
+ effectiveDuration={props.effectiveDuration}
109
+ majorTickInterval={props.majorTickInterval}
110
+ theme={props.theme}
111
+ beatAnalysis={props.beatAnalysis}
228
112
  />
229
113
 
230
- {
231
- // fallow-ignore-next-line complexity
232
- displayTrackOrder.map((layerId, rowIndex) => {
233
- const layer = tracks.find((item) => item.id === layerId) ?? null;
234
- const previousLayerId = displayTrackOrder[rowIndex - 1];
235
- const previousLayer = previousLayerId
236
- ? (tracks.find((item) => item.id === previousLayerId) ?? null)
237
- : null;
238
- const els = layer?.elements ?? [];
239
- const ts = trackStyles.get(layerId) ?? getTrackStyle("");
240
- const isAudioLayer = layer?.kind === "audio";
241
- const isFirstAudioLayer = isAudioLayer && previousLayer?.kind !== "audio";
242
- const isPendingTrack =
243
- draggedClip?.started === true && !trackOrder.includes(layerId) && els.length === 0;
244
- const baseRowBackground = rowIndex % 2 === 0 ? theme.rowBackground : "#0D0E12";
245
- const dropIndicator = resolveTimelineDropIndicator({
246
- placement: activeDropPlacement,
247
- layerId,
248
- layerOrder: displayTrackOrder,
249
- });
250
- const rowBackground =
251
- dropIndicator?.kind === "onto"
252
- ? theme.clipBackgroundActive
253
- : isAudioLayer
254
- ? `linear-gradient(90deg, ${theme.gutterBackground} 0, ${baseRowBackground} 74px)`
255
- : baseRowBackground;
256
- const showGroupHeader = shouldShowTimelineLayerGroupHeader(
257
- layer?.contextKey ?? "",
258
- previousLayer?.contextKey ?? "",
259
- );
260
- const rowTrack = layer?.placementTrack ?? els[0]?.track ?? 0;
261
- // The beat-dot strip occupies the top of this track's lane (active track,
262
- // or the music track when nothing is selected). When shown, keyframe
263
- // diamonds shrink + drop to the bottom half so they don't collide with it.
264
- const beatStripOnTrack =
265
- (beatAnalysis?.beatTimes?.length ?? 0) >= 2 &&
266
- (selectedElementId
267
- ? els.some((e) => (e.key ?? e.id) === selectedElementId)
268
- : els.some(isMusicTrack));
269
- const isTrackHidden = els.length > 0 && els.every((element) => element.hidden === true);
270
- return (
271
- <Fragment key={layerId}>
272
- {showGroupHeader && layer && (
273
- <TimelineLayerGroupHeader
274
- contextKey={layer.contextKey}
275
- trackContentWidth={trackContentWidth}
276
- theme={theme}
277
- accentColor={ts.accent}
278
- />
279
- )}
280
- <div
281
- className="relative flex"
282
- style={{
283
- height: TRACK_H,
284
- background: rowBackground,
285
- borderTop: isFirstAudioLayer ? `2px solid ${theme.rulerBorder}` : undefined,
286
- borderBottom: `1px solid ${theme.rowBorder}`,
287
- boxShadow:
288
- dropIndicator?.kind === "onto" ? `inset 0 0 0 1px ${ts.accent}` : undefined,
289
- }}
290
- >
291
- <TimelineLayerGutter
292
- isAudio={isAudioLayer}
293
- isTrackHidden={isTrackHidden}
294
- rowTrack={rowTrack}
295
- theme={theme}
296
- onToggleHidden={() => {
297
- if (onToggleElementHidden && els.length > 0) {
298
- for (const element of els) {
299
- void onToggleElementHidden(element.key ?? element.id, !isTrackHidden);
300
- }
301
- return;
302
- }
303
- void onToggleTrackHidden?.(rowTrack, !isTrackHidden);
304
- }}
305
- />
306
- <div
307
- style={{
308
- width: trackContentWidth,
309
- opacity: isTrackHidden ? 0.35 : 1,
310
- transition: "opacity 120ms ease",
311
- }}
312
- className="relative"
313
- >
314
- {layer?.contextKey && (
315
- <span
316
- className="absolute bottom-0 top-0 pointer-events-none"
317
- style={{
318
- left: 0,
319
- width: 2,
320
- background: ts.accent,
321
- opacity: 0.45,
322
- zIndex: 2,
323
- }}
324
- />
325
- )}
326
- {dropIndicator?.kind === "line" && (
327
- <TimelineDropInsertionLine edge={dropIndicator.edge} accentColor={ts.accent} />
328
- )}
329
- {/* Faint beat lines in every track's background (behind the clips);
330
- the active snap target is highlighted. */}
331
- <BeatBackgroundLines
332
- beatTimes={beatAnalysis?.beatTimes}
333
- beatStrengths={beatAnalysis?.beatStrengths}
334
- pps={pps}
335
- highlightTime={activeSnapGuideTime}
336
- />
337
- {/* Beat dots on the active track (the one holding the selection),
338
- falling back to the music track when nothing is selected. */}
339
- {beatStripOnTrack && (
340
- <BeatStrip
341
- beatTimes={beatAnalysis?.beatTimes}
342
- beatStrengths={beatAnalysis?.beatStrengths}
343
- pps={pps}
344
- />
345
- )}
346
- {isPendingTrack && (
347
- <div
348
- className="absolute inset-0 flex items-center"
349
- style={{
350
- paddingLeft: 16,
351
- color: ts.label,
352
- fontSize: 11,
353
- letterSpacing: 0,
354
- textTransform: "uppercase",
355
- opacity: 0.5,
356
- }}
357
- >
358
- New track
359
- </div>
360
- )}
361
- {
362
- // fallow-ignore-next-line complexity
363
- els.map((el) => {
364
- const clipStyle = getTrackStyle(el.tag);
365
- const elementKey = el.key ?? el.id;
366
- const capabilities = getTimelineEditCapabilities(el);
367
- const isSelected = selectedElementIds.has(elementKey);
368
- const isComposition = !!el.compositionSrc;
369
- // elementKey (el.key ?? el.id) is already unique per clip; do NOT
370
- // fold in the map index, or a splice/reorder remounts every clip
371
- // at/after the change (DOM flash, drag interruption).
372
- const clipKey = elementKey;
373
- const isDraggingClip =
374
- draggedClip?.started === true &&
375
- (draggedElement?.key ?? draggedElement?.id) === elementKey;
376
- if (isDraggingClip) return null;
377
- const previewElement = getPreviewElement(el);
378
- return (
379
- <TimelineClip
380
- key={clipKey}
381
- onContextMenu={(e: React.MouseEvent) => {
382
- e.preventDefault();
383
- onContextMenuClip?.(e, el);
384
- }}
385
- el={previewElement}
386
- pps={pps}
387
- clipY={CLIP_Y}
388
- isSelected={isSelected}
389
- isHovered={hoveredClip === clipKey}
390
- isDragging={false}
391
- hasCustomContent={!!renderClipContent}
392
- capabilities={capabilities}
393
- theme={theme}
394
- isComposition={isComposition}
395
- onHoverStart={() => setHoveredClip(clipKey)}
396
- onHoverEnd={() => setHoveredClip(null)}
397
- onResizeStart={(edge, e) => {
398
- if (e.button !== 0 || e.shiftKey || !onResizeElement) return;
399
- if (edge === "start" && !capabilities.canTrimStart) return;
400
- if (edge === "end" && !capabilities.canTrimEnd) return;
401
- e.stopPropagation();
402
- blockedClipRef.current = null;
403
- setShowPopover(false);
404
- setRangeSelection(null);
405
- setResizingClip({
406
- element: el,
407
- edge,
408
- originClientX: e.clientX,
409
- previewStart: el.start,
410
- previewDuration: el.duration,
411
- previewPlaybackStart: el.playbackStart,
412
- snapGuideTime: null,
413
- snapGuideKind: null,
414
- started: false,
415
- });
416
- }}
417
- onPointerDown={
418
- // fallow-ignore-next-line complexity
419
- (e) => {
420
- if (e.button !== 0) return;
421
- if (usePlayerStore.getState().activeTool === "razor") return;
422
- if (e.shiftKey) {
423
- shiftClickClipRef.current = {
424
- element: el,
425
- anchorX: e.clientX,
426
- anchorY: e.clientY,
427
- };
428
- return;
429
- }
430
- const target = e.currentTarget as HTMLElement;
431
- const rect = target.getBoundingClientRect();
432
- const blockedIntent = resolveBlockedTimelineEditIntent({
433
- width: rect.width,
434
- offsetX: e.clientX - rect.left,
435
- handleWidth: CLIP_HANDLE_W,
436
- capabilities,
437
- });
438
- if (
439
- blockedIntent &&
440
- ((blockedIntent === "move" && onMoveElement) ||
441
- (blockedIntent !== "move" && onResizeElement))
442
- ) {
443
- blockedClipRef.current = {
444
- element: el,
445
- intent: blockedIntent,
446
- originClientX: e.clientX,
447
- originClientY: e.clientY,
448
- started: false,
449
- };
450
- return;
451
- }
452
- if (!onMoveElement || !capabilities.canMove) return;
453
- blockedClipRef.current = null;
454
- setShowPopover(false);
455
- setRangeSelection(null);
456
- setDraggedClip({
457
- element: el,
458
- originClientX: e.clientX,
459
- originClientY: e.clientY,
460
- originScrollLeft: scrollRef.current?.scrollLeft ?? 0,
461
- originScrollTop: scrollRef.current?.scrollTop ?? 0,
462
- pointerClientX: e.clientX,
463
- pointerClientY: e.clientY,
464
- pointerOffsetX: e.clientX - rect.left,
465
- pointerOffsetY: e.clientY - rect.top,
466
- previewStart: el.start,
467
- previewTrack: el.track,
468
- previewLayerId: layerId,
469
- previewLayerIndex: rowIndex,
470
- previewStackingReorder: null,
471
- snapBeatTime: null,
472
- snapGuideTime: null,
473
- snapGuideKind: null,
474
- started: false,
475
- });
476
- syncClipDragAutoScroll(e.clientX, e.clientY);
477
- }
478
- }
479
- onClick={(e) => {
480
- e.stopPropagation();
481
- if (suppressClickRef.current) return;
482
- const { activeTool } = usePlayerStore.getState();
483
- if (activeTool === "razor" && onRazorSplit) {
484
- const clipRect = (
485
- e.currentTarget as HTMLElement
486
- ).getBoundingClientRect();
487
- const clickOffsetX = e.clientX - clipRect.left;
488
- const splitTime = previewElement.start + clickOffsetX / pps;
489
- const clampedTime = Math.max(
490
- previewElement.start + SPLIT_BOUNDARY_EPSILON_S,
491
- Math.min(
492
- previewElement.start +
493
- previewElement.duration -
494
- SPLIT_BOUNDARY_EPSILON_S,
495
- splitTime,
496
- ),
497
- );
498
- if (e.shiftKey && onRazorSplitAll) {
499
- onRazorSplitAll(clampedTime);
500
- } else {
501
- onRazorSplit(el, clampedTime);
502
- }
503
- return;
504
- }
505
- const nextElement = isSelected ? null : el;
506
- setSelectedElementId(nextElement ? elementKey : null);
507
- onSelectElement?.(nextElement);
508
- }}
509
- onDoubleClick={(e) => {
510
- e.stopPropagation();
511
- if (suppressClickRef.current) return;
512
- if (isComposition && onDrillDown) onDrillDown(el);
513
- }}
514
- >
515
- {renderClipChildren(previewElement, clipStyle)}
516
- {STUDIO_KEYFRAMES_ENABLED && keyframeCache?.get(elementKey) && (
517
- <TimelineClipDiamonds
518
- keyframesData={keyframeCache.get(elementKey)!}
519
- clipWidthPx={Math.max(previewElement.duration * pps, 4)}
520
- clipHeightPx={TRACK_H - 2 * CLIP_Y}
521
- beatsActive={beatStripOnTrack}
522
- beatTimes={beatAnalysis?.beatTimes}
523
- clipStart={previewElement.start}
524
- clipDurationSeconds={previewElement.duration}
525
- pixelsPerSecond={pps}
526
- accentColor={clipStyle.accent}
527
- isSelected={isSelected}
528
- currentPercentage={
529
- previewElement.duration > 0
530
- ? ((currentTime - previewElement.start) /
531
- previewElement.duration) *
532
- 100
533
- : 0
534
- }
535
- elementId={elementKey}
536
- selectedKeyframes={selectedKeyframes}
537
- onClickKeyframe={(pct) => onClickKeyframe?.(previewElement, pct)}
538
- onShiftClickKeyframe={onShiftClickKeyframe}
539
- onContextMenuKeyframe={onContextMenuKeyframe}
540
- onMoveKeyframe={onMoveKeyframe}
541
- suppressClickRef={suppressClickRef}
542
- />
543
- )}
544
- </TimelineClip>
545
- );
546
- })
547
- }
548
- </div>
549
- </div>
550
- </Fragment>
551
- );
552
- })
553
- }
114
+ {/* Breathing room between the sticky ruler and the first track lane — the
115
+ top half of the CapCut-style padding (see TRACKS_TOP_PAD). */}
116
+ <div aria-hidden="true" style={{ height: TRACKS_TOP_PAD }} />
117
+
118
+ <TimelineLanes
119
+ {...props}
120
+ draggedElement={draggedElement}
121
+ multiDragPreview={multiDragPreview}
122
+ onToggleTrackHidden={onToggleTrackHidden}
123
+ onResizeElement={onResizeElement}
124
+ onMoveElement={onMoveElement}
125
+ onRazorSplit={onRazorSplit}
126
+ onRazorSplitAll={onRazorSplitAll}
127
+ />
128
+
129
+ {/* Breathing room below the last track lane (~1.5 track heights) — a real
130
+ scrollable surface, so a clip can be dragged into the void to create a
131
+ new bottom track comfortably (see TRACKS_BOTTOM_PAD / getTimelineCanvasHeight). */}
132
+ <div aria-hidden="true" style={{ height: TRACKS_BOTTOM_PAD }} />
133
+
134
+ {/* Drop placeholder — a clip-sized slot at the exact landing spot (target
135
+ lane + snapped start), parallel to the ghost. Hidden in insert mode. */}
136
+ {draggedClip?.started && draggedClip.insertRow == null && draggedRowIndex >= 0 && (
137
+ <div
138
+ className="absolute pointer-events-none"
139
+ style={{
140
+ top: getTimelineRowTop(draggedRowIndex) + CLIP_Y,
141
+ left: GUTTER + draggedClip.previewStart * props.pps,
142
+ width: Math.max(draggedClip.element.duration * props.pps, 4),
143
+ height: TRACK_H - CLIP_Y * 2,
144
+ border: "1px solid rgba(60,230,172,0.55)",
145
+ background: "rgba(60,230,172,0.12)",
146
+ borderRadius: 4,
147
+ zIndex: 30,
148
+ }}
149
+ />
150
+ )}
151
+
152
+ {/* Insertion line — a new track will be inserted at this boundary on drop.
153
+ Shown while the pointer is near a lane boundary (insert mode). */}
154
+ {draggedClip?.started && draggedClip.insertRow != null && (
155
+ <div
156
+ className="absolute pointer-events-none"
157
+ style={{
158
+ top: getTimelineRowTop(draggedClip.insertRow) - 0.5,
159
+ left: GUTTER,
160
+ width: props.trackContentWidth,
161
+ height: 1,
162
+ background: "#3CE6AC",
163
+ boxShadow: "0 0 3px rgba(60,230,172,0.5)",
164
+ zIndex: 55,
165
+ }}
166
+ />
167
+ )}
168
+
169
+ {/* Snap guide for non-beat targets during clip drag */}
170
+ {draggedClip?.started && draggedClip.snapTime != null && draggedClip.snapType !== "beat" && (
171
+ <div
172
+ className="absolute pointer-events-none"
173
+ style={{
174
+ left: GUTTER + draggedClip.snapTime * props.pps,
175
+ top: RULER_H,
176
+ bottom: 0,
177
+ width: 1,
178
+ background: draggedClip.snapType === "playhead" ? "#3CE6AC" : "rgba(255,255,255,0.6)",
179
+ boxShadow:
180
+ draggedClip.snapType === "playhead"
181
+ ? "0 0 6px rgba(60,230,172,0.5)"
182
+ : "0 0 6px rgba(255,255,255,0.4)",
183
+ zIndex: 60,
184
+ }}
185
+ />
186
+ )}
554
187
 
188
+ {/* Drag ghost */}
555
189
  {activeDraggedElement && activeDraggedPosition && (
556
- <TimelineDragGhost
557
- element={activeDraggedElement}
558
- position={activeDraggedPosition}
559
- pps={pps}
560
- selectedElementId={selectedElementId}
561
- hasCustomContent={!!renderClipContent}
562
- theme={theme}
190
+ <div
191
+ className="absolute pointer-events-none"
192
+ style={{
193
+ top: activeDraggedPosition.top,
194
+ left: activeDraggedPosition.left,
195
+ width: Math.max(activeDraggedElement.duration * props.pps, 4),
196
+ height: TRACK_H - CLIP_Y * 2,
197
+ zIndex: 40,
198
+ }}
563
199
  >
564
- {renderClipChildren(activeDraggedElement, getTrackStyle(activeDraggedElement.tag))}
565
- </TimelineDragGhost>
200
+ <TimelineClip
201
+ el={{ ...activeDraggedElement, start: 0 }}
202
+ pps={props.pps}
203
+ clipY={0}
204
+ isSelected={
205
+ props.selectedElementId === (activeDraggedElement.key ?? activeDraggedElement.id)
206
+ }
207
+ isHovered={false}
208
+ isDragging={true}
209
+ hasCustomContent={!!props.renderClipContent}
210
+ capabilities={getTimelineEditCapabilities(activeDraggedElement)}
211
+ theme={props.theme}
212
+ isComposition={!!activeDraggedElement.compositionSrc}
213
+ onHoverStart={() => {}}
214
+ onHoverEnd={() => {}}
215
+ onResizeStart={() => {}}
216
+ onClick={() => {}}
217
+ onDoubleClick={() => {}}
218
+ >
219
+ {renderClipChildren(
220
+ activeDraggedElement,
221
+ props.getTrackStyle(activeDraggedElement.tag),
222
+ props.renderClipContent,
223
+ props.renderClipOverlay,
224
+ )}
225
+ </TimelineClip>
226
+ </div>
566
227
  )}
567
228
 
568
- <TimelineSelectionOverlays
569
- rangeSelection={rangeSelection}
570
- marqueeRect={marqueeRect}
571
- pps={pps}
572
- accentColor={getTrackStyle("").accent}
573
- />
229
+ {/* Marquee (rubber-band) multi-select rectangle — mirrors the canvas
230
+ MarqueeOverlay look: semi-transparent accent fill + dashed border. */}
231
+ {props.marqueeRect && (
232
+ <div
233
+ aria-hidden="true"
234
+ className="absolute pointer-events-none"
235
+ style={{
236
+ left: props.marqueeRect.left,
237
+ top: props.marqueeRect.top,
238
+ width: props.marqueeRect.width,
239
+ height: props.marqueeRect.height,
240
+ background: "rgba(60,230,172,0.10)",
241
+ border: "1px dashed rgba(60,230,172,0.7)",
242
+ borderRadius: 2,
243
+ zIndex: 70,
244
+ }}
245
+ />
246
+ )}
247
+
248
+ {/* Range highlight */}
249
+ {props.rangeSelection && (
250
+ <div
251
+ className="absolute pointer-events-none"
252
+ style={{
253
+ left:
254
+ GUTTER + Math.min(props.rangeSelection.start, props.rangeSelection.end) * props.pps,
255
+ width: Math.abs(props.rangeSelection.end - props.rangeSelection.start) * props.pps,
256
+ top: RULER_H,
257
+ bottom: 0,
258
+ backgroundColor: "rgba(59, 130, 246, 0.12)",
259
+ borderLeft: "1px solid rgba(59, 130, 246, 0.4)",
260
+ borderRight: "1px solid rgba(59, 130, 246, 0.4)",
261
+ zIndex: 50,
262
+ }}
263
+ />
264
+ )}
574
265
 
575
266
  {/* Playhead — hidden while dragging a beat so its guideline doesn't
576
- track the scrub and clutter the beat being moved. */}
267
+ track the scrub and clutter the beat being moved. Explicit width +
268
+ the half-head offset baked into getTimelinePlayheadLeft keep the
269
+ inner 1px line's CENTER exactly on GUTTER + t * pps (the ruler
270
+ ticks' center), instead of relying on shrink-wrap sizing. */}
577
271
  <div
578
- ref={playheadRef}
272
+ ref={props.playheadRef}
579
273
  className="absolute top-0 bottom-0 pointer-events-none"
580
274
  style={{
581
- left: `${GUTTER}px`,
275
+ left: `${getTimelinePlayheadLeft(0, 0)}px`,
276
+ width: PLAYHEAD_HEAD_W,
582
277
  zIndex: 100,
583
278
  display: beatDragging ? "none" : undefined,
584
279
  }}
585
280
  >
586
- <PlayheadIndicator />
281
+ <PlayheadIndicator scrubbing={props.isScrubbing} />
587
282
  </div>
588
283
  </div>
589
284
  );