@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.
Files changed (262) hide show
  1. package/dist/assets/{index-uBY329wb.js → index-BWnOxAiH.js} +1 -1
  2. package/dist/assets/{index-CMHYjEZ5.js → index-C4csZims.js} +1 -1
  3. package/dist/assets/index-CmVCjZjp.js +423 -0
  4. package/dist/assets/index-D-GyYi2d.css +1 -0
  5. package/dist/{chunk-SOTCF4DF.js → chunk-5QSIMBEJ.js} +2 -1
  6. package/dist/chunk-5QSIMBEJ.js.map +1 -0
  7. package/dist/{domEditingLayers-2ECJK24D.js → domEditingLayers-6LQGKPOI.js} +2 -2
  8. package/dist/index.d.ts +146 -144
  9. package/dist/index.html +2 -2
  10. package/dist/index.js +40582 -37803
  11. package/dist/index.js.map +1 -1
  12. package/package.json +7 -7
  13. package/src/App.tsx +106 -96
  14. package/src/components/EditorShell.tsx +253 -0
  15. package/src/components/StudioGlobalDragOverlay.tsx +1 -3
  16. package/src/components/StudioLeftSidebar.tsx +13 -3
  17. package/src/components/StudioRightPanel.tsx +8 -3
  18. package/src/components/TimelineToolbar.test.tsx +2 -2
  19. package/src/components/TimelineToolbar.tsx +179 -121
  20. package/src/components/editor/CanvasContextMenu.test.tsx +115 -0
  21. package/src/components/editor/CanvasContextMenu.tsx +198 -0
  22. package/src/components/editor/DomEditCropHandles.test.tsx +74 -3
  23. package/src/components/editor/DomEditCropHandles.tsx +64 -29
  24. package/src/components/editor/DomEditOverlay.test.ts +245 -279
  25. package/src/components/editor/DomEditOverlay.tsx +158 -126
  26. package/src/components/editor/DomEditOverlayHover.test.tsx +90 -0
  27. package/src/components/editor/DomEditRotateHandle.tsx +38 -18
  28. package/src/components/editor/DomEditSelectionChrome.test.tsx +80 -0
  29. package/src/components/editor/DomEditSelectionChrome.tsx +257 -0
  30. package/src/components/editor/LayersPanel.test.ts +66 -2
  31. package/src/components/editor/LayersPanel.tsx +43 -1
  32. package/src/components/editor/OffCanvasIndicators.test.tsx +34 -0
  33. package/src/components/editor/OffCanvasIndicators.tsx +49 -10
  34. package/src/components/editor/SnapGuideOverlay.tsx +28 -23
  35. package/src/components/editor/SnapToolbar.test.tsx +0 -1
  36. package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +214 -0
  37. package/src/components/editor/anchoredResizeReleaseShift.test.ts +208 -0
  38. package/src/components/editor/canvasContextMenuZOrder.test.ts +435 -0
  39. package/src/components/editor/canvasContextMenuZOrder.ts +352 -0
  40. package/src/components/editor/domEditNudge.test.ts +80 -0
  41. package/src/components/editor/domEditNudge.ts +44 -0
  42. package/src/components/editor/domEditOverlayGeometry.test.ts +167 -1
  43. package/src/components/editor/domEditOverlayGeometry.ts +280 -20
  44. package/src/components/editor/domEditOverlayGestures.ts +44 -41
  45. package/src/components/editor/domEditOverlayStartGesture.ts +62 -2
  46. package/src/components/editor/domEditResizeLocal.test.ts +131 -0
  47. package/src/components/editor/domEditResizeLocal.ts +125 -0
  48. package/src/components/editor/domEditingDom.ts +1 -1
  49. package/src/components/editor/manualEditsDomPatches.test.ts +47 -0
  50. package/src/components/editor/manualOffsetDrag.ts +55 -12
  51. package/src/components/editor/offCanvasIndicatorGeometry.ts +31 -10
  52. package/src/components/editor/offCanvasIndicatorRefresh.test.tsx +25 -1
  53. package/src/components/editor/resizeDraft.ts +108 -0
  54. package/src/components/editor/snapEngine.test.ts +12 -78
  55. package/src/components/editor/snapEngine.ts +13 -53
  56. package/src/components/editor/useDomEditNudge.test.tsx +228 -0
  57. package/src/components/editor/useDomEditNudge.ts +254 -0
  58. package/src/components/editor/useDomEditOverlayGestures.ts +115 -116
  59. package/src/components/editor/useDomEditOverlayRects.ts +9 -2
  60. package/src/components/nle/AssetPreviewOverlay.tsx +147 -0
  61. package/src/components/nle/NLEContext.test.ts +144 -0
  62. package/src/components/nle/NLEContext.tsx +328 -0
  63. package/src/components/nle/NLEPreview.tsx +1 -1
  64. package/src/components/nle/PreviewOverlays.tsx +240 -0
  65. package/src/components/nle/PreviewPane.tsx +163 -0
  66. package/src/components/nle/TimelinePane.test.ts +60 -0
  67. package/src/components/nle/TimelinePane.tsx +293 -0
  68. package/src/components/nle/TimelineResizeDivider.tsx +8 -2
  69. package/src/components/nle/useCompositionStack.test.tsx +44 -0
  70. package/src/components/nle/useCompositionStack.ts +17 -9
  71. package/src/components/nle/useTimelineEditCallbacks.ts +217 -0
  72. package/src/components/sidebar/AssetCard.tsx +326 -0
  73. package/src/components/sidebar/AssetContextMenu.tsx +15 -30
  74. package/src/components/sidebar/AssetsTab.test.ts +96 -0
  75. package/src/components/sidebar/AssetsTab.tsx +71 -206
  76. package/src/components/sidebar/AudioRow.tsx +39 -2
  77. package/src/components/sidebar/BlocksTab.tsx +8 -1
  78. package/src/components/sidebar/LeftSidebar.tsx +4 -1
  79. package/src/components/sidebar/assetHelpers.ts +29 -0
  80. package/src/contexts/DomEditContext.tsx +9 -0
  81. package/src/contexts/StudioContext.tsx +9 -8
  82. package/src/contexts/TimelineEditContext.tsx +3 -7
  83. package/src/hooks/deleteSelectedKeyframes.ts +35 -0
  84. package/src/hooks/domEditCommitTypes.ts +12 -1
  85. package/src/hooks/domSelectionTestHarness.ts +14 -0
  86. package/src/hooks/gestureTransaction.test.ts +311 -0
  87. package/src/hooks/gestureTransaction.ts +199 -0
  88. package/src/hooks/gsapDragCommit.test.ts +141 -41
  89. package/src/hooks/gsapDragCommit.ts +64 -74
  90. package/src/hooks/gsapDragStaticSetHelpers.ts +9 -21
  91. package/src/hooks/gsapResizeIntercept.test.ts +69 -0
  92. package/src/hooks/gsapResizeIntercept.ts +38 -10
  93. package/src/hooks/gsapRuntimeBridge.test.ts +49 -13
  94. package/src/hooks/gsapRuntimeBridge.ts +4 -4
  95. package/src/hooks/gsapScriptCommitTypes.ts +16 -5
  96. package/src/hooks/gsapShared.test.ts +19 -1
  97. package/src/hooks/gsapShared.ts +14 -0
  98. package/src/hooks/patchDocumentRootDuration.test.ts +71 -0
  99. package/src/hooks/timelineEditingGsap.ts +14 -0
  100. package/src/hooks/timelineEditingHelpers.test.ts +34 -1
  101. package/src/hooks/timelineEditingHelpers.ts +22 -48
  102. package/src/hooks/timelineMoveAdapter.test.ts +77 -0
  103. package/src/hooks/timelineMoveAdapter.ts +37 -0
  104. package/src/hooks/useAnimatedPropertyCommit.test.tsx +36 -8
  105. package/src/hooks/useAnimatedPropertyCommit.ts +31 -26
  106. package/src/hooks/useAppHotkeys.ts +16 -25
  107. package/src/hooks/useBlockHandlers.ts +55 -27
  108. package/src/hooks/useContextMenuDismiss.ts +31 -7
  109. package/src/hooks/useDomEditCommits.test.tsx +154 -1
  110. package/src/hooks/useDomEditCommits.ts +100 -3
  111. package/src/hooks/useDomEditPositionPatchCommit.ts +2 -0
  112. package/src/hooks/useDomEditSession.ts +2 -2
  113. package/src/hooks/useDomEditTextCommits.test.tsx +135 -0
  114. package/src/hooks/useDomEditTextCommits.ts +8 -3
  115. package/src/hooks/useDomEditWiring.ts +7 -9
  116. package/src/hooks/useDomGeometryCommits.ts +17 -2
  117. package/src/hooks/useDomSelection.test.ts +2 -36
  118. package/src/hooks/useDomSelection.ts +81 -51
  119. package/src/hooks/useDomSelectionSelectionGuards.test.ts +276 -0
  120. package/src/hooks/useElementLifecycleOps.test.tsx +258 -0
  121. package/src/hooks/useElementLifecycleOps.ts +40 -71
  122. package/src/hooks/useEnableKeyframes.test.ts +142 -1
  123. package/src/hooks/useEnableKeyframes.ts +73 -29
  124. package/src/hooks/useGestureCommit.test.tsx +130 -0
  125. package/src/hooks/useGestureCommit.ts +32 -8
  126. package/src/hooks/useGroupCommits.ts +7 -28
  127. package/src/hooks/useGsapAwareEditing.test.tsx +239 -0
  128. package/src/hooks/useGsapAwareEditing.ts +115 -20
  129. package/src/hooks/useGsapKeyframeOps.test.tsx +59 -0
  130. package/src/hooks/useGsapKeyframeOps.ts +38 -10
  131. package/src/hooks/useGsapScriptCommits.test.tsx +40 -1
  132. package/src/hooks/useGsapScriptCommits.ts +134 -50
  133. package/src/hooks/useGsapSelectionHandlers.ts +49 -16
  134. package/src/hooks/useMusicBeatAnalysis.ts +72 -36
  135. package/src/hooks/usePersistentEditHistory.test.ts +33 -0
  136. package/src/hooks/usePersistentEditHistory.ts +92 -58
  137. package/src/hooks/usePreviewPersistence.ts +25 -7
  138. package/src/hooks/useRazorSplit.history.test.tsx +303 -0
  139. package/src/hooks/useRazorSplit.test.ts +269 -0
  140. package/src/hooks/useRazorSplit.testHelpers.ts +64 -0
  141. package/src/hooks/useRazorSplit.ts +107 -36
  142. package/src/hooks/useRenderClipContent.ts +24 -6
  143. package/src/hooks/useStudioContextValue.ts +15 -5
  144. package/src/hooks/useStudioUrlState.ts +2 -4
  145. package/src/hooks/useTimelineEditing.test.tsx +20 -29
  146. package/src/hooks/useTimelineEditingTypes.ts +6 -0
  147. package/src/hooks/useTimelineGroupEditing.ts +18 -4
  148. package/src/index.ts +1 -1
  149. package/src/player/components/ImageThumbnail.test.tsx +173 -0
  150. package/src/player/components/ImageThumbnail.tsx +160 -0
  151. package/src/player/components/PlayheadIndicator.tsx +50 -5
  152. package/src/player/components/Timeline.test.ts +102 -19
  153. package/src/player/components/Timeline.tsx +194 -194
  154. package/src/player/components/TimelineCanvas.tsx +207 -521
  155. package/src/player/components/TimelineClip.tsx +4 -1
  156. package/src/player/components/TimelineClipDiamonds.tsx +9 -40
  157. package/src/player/components/TimelineEmptyState.tsx +3 -1
  158. package/src/player/components/TimelineLanes.tsx +487 -0
  159. package/src/player/components/TimelineOverlays.tsx +122 -0
  160. package/src/player/components/VideoThumbnail.test.tsx +152 -0
  161. package/src/player/components/VideoThumbnail.tsx +28 -7
  162. package/src/player/components/thumbnailUtils.test.ts +127 -0
  163. package/src/player/components/thumbnailUtils.ts +54 -0
  164. package/src/player/components/timelineCallbacks.ts +18 -18
  165. package/src/player/components/timelineClipChildren.tsx +39 -0
  166. package/src/player/components/timelineClipDragCommit.test.ts +943 -0
  167. package/src/player/components/timelineClipDragCommit.ts +401 -0
  168. package/src/player/components/timelineClipDragPreview.test.ts +144 -0
  169. package/src/player/components/timelineClipDragPreview.ts +323 -38
  170. package/src/player/components/timelineClipDragTypes.ts +65 -0
  171. package/src/player/components/timelineCollision.test.ts +477 -0
  172. package/src/player/components/timelineCollision.ts +263 -0
  173. package/src/player/components/timelineDragDrop.ts +69 -60
  174. package/src/player/components/timelineEditCapabilities.ts +52 -0
  175. package/src/player/components/timelineEditing.test.ts +1 -1
  176. package/src/player/components/timelineEditing.ts +76 -59
  177. package/src/player/components/timelineGroupEditing.test.ts +126 -0
  178. package/src/player/components/timelineGroupEditing.ts +133 -0
  179. package/src/player/components/timelineGroupResizeCommit.ts +54 -0
  180. package/src/player/components/timelineLayout.test.ts +95 -23
  181. package/src/player/components/timelineLayout.ts +120 -61
  182. package/src/player/components/timelineMarquee.test.ts +197 -0
  183. package/src/player/components/timelineMarquee.ts +112 -0
  184. package/src/player/components/timelineMultiDragPreview.test.ts +127 -0
  185. package/src/player/components/timelineMultiDragPreview.ts +106 -0
  186. package/src/player/components/timelineOptimisticRevision.ts +42 -0
  187. package/src/player/components/timelineSnapping.test.ts +134 -0
  188. package/src/player/components/timelineSnapping.ts +110 -0
  189. package/src/player/components/timelineStackingSync.test.ts +244 -0
  190. package/src/player/components/timelineStackingSync.ts +331 -0
  191. package/src/player/components/timelineTheme.ts +6 -1
  192. package/src/player/components/timelineZones.test.ts +425 -0
  193. package/src/player/components/timelineZones.ts +198 -0
  194. package/src/player/components/timelineZoom.test.ts +67 -0
  195. package/src/player/components/timelineZoom.ts +51 -0
  196. package/src/player/components/useResolvedTimelineEditCallbacks.ts +25 -3
  197. package/src/player/components/useTimelineClipDrag.resize.test.tsx +241 -0
  198. package/src/player/components/useTimelineClipDrag.ts +380 -386
  199. package/src/player/components/useTimelineEditPinning.ts +121 -0
  200. package/src/player/components/useTimelineGeometry.ts +129 -0
  201. package/src/player/components/useTimelinePlayhead.ts +4 -16
  202. package/src/player/components/useTimelineRangeSelection.ts +341 -50
  203. package/src/player/components/useTimelineStackingSync.test.tsx +67 -0
  204. package/src/player/components/useTimelineStackingSync.ts +83 -0
  205. package/src/player/hooks/useExpandedTimelineElements.ts +0 -3
  206. package/src/player/hooks/usePlaybackKeyboard.ts +5 -0
  207. package/src/player/hooks/useTimelinePlayer.ts +48 -53
  208. package/src/player/hooks/useTimelinePlayerLoop.ts +85 -0
  209. package/src/player/hooks/useTimelineSyncCallbacks.test.ts +219 -0
  210. package/src/player/hooks/useTimelineSyncCallbacks.ts +104 -4
  211. package/src/player/index.ts +3 -2
  212. package/src/player/lib/time.test.ts +19 -19
  213. package/src/player/lib/time.ts +6 -2
  214. package/src/player/lib/timelineDOM.test.ts +25 -98
  215. package/src/player/lib/timelineDOM.ts +12 -81
  216. package/src/player/lib/timelineElementHelpers.ts +54 -5
  217. package/src/player/lib/timelineIframeHelpers.ts +2 -0
  218. package/src/player/store/playerStore.ts +62 -3
  219. package/src/styles/studio.css +27 -0
  220. package/src/utils/assetClickBehavior.test.ts +104 -0
  221. package/src/utils/assetClickBehavior.ts +75 -0
  222. package/src/utils/assetPreviewStore.ts +33 -0
  223. package/src/utils/blockInstaller.ts +10 -2
  224. package/src/utils/canvasNudgeGate.test.ts +31 -0
  225. package/src/utils/canvasNudgeGate.ts +32 -0
  226. package/src/utils/editHistory.test.ts +35 -0
  227. package/src/utils/gsapSoftReload.test.ts +119 -1
  228. package/src/utils/gsapSoftReload.ts +153 -0
  229. package/src/utils/mediaTypes.ts +3 -2
  230. package/src/utils/resizeDebug.ts +55 -0
  231. package/src/utils/rootDuration.test.ts +90 -1
  232. package/src/utils/rootDuration.ts +76 -13
  233. package/src/utils/sdkCutover.ts +4 -0
  234. package/src/utils/studioFileHistory.ts +37 -7
  235. package/src/utils/studioHelpers.ts +62 -0
  236. package/src/utils/studioPreviewHelpers.test.ts +65 -8
  237. package/src/utils/studioPreviewHelpers.ts +10 -0
  238. package/src/utils/studioTelemetry.ts +4 -1
  239. package/src/utils/studioUiPreferences.test.ts +38 -0
  240. package/src/utils/studioUiPreferences.ts +27 -0
  241. package/src/utils/studioUrlState.test.ts +0 -1
  242. package/src/utils/timelineAssetDrop.ts +62 -0
  243. package/src/utils/timelineDiscovery.ts +0 -17
  244. package/src/utils/timelineInspector.test.ts +121 -0
  245. package/src/utils/timelineInspector.ts +32 -1
  246. package/dist/assets/index-Dq7FEg0K.css +0 -1
  247. package/dist/assets/index-pRhCpGPz.js +0 -423
  248. package/dist/chunk-SOTCF4DF.js.map +0 -1
  249. package/src/components/StudioPreviewArea.tsx +0 -500
  250. package/src/components/nle/NLELayout.test.ts +0 -12
  251. package/src/components/nle/NLELayout.tsx +0 -591
  252. package/src/player/components/TimelineLayerGutter.tsx +0 -61
  253. package/src/player/components/TimelineSelectionOverlays.tsx +0 -55
  254. package/src/player/components/timelineMarqueeSelection.test.ts +0 -87
  255. package/src/player/components/timelineSnapTargets.test.ts +0 -144
  256. package/src/player/components/timelineSnapTargets.ts +0 -164
  257. package/src/player/components/useTimelineClipDrag.test.tsx +0 -506
  258. package/src/player/components/useTimelineClipGroupDrag.ts +0 -417
  259. package/src/player/components/useTimelineMarqueeSelection.test.tsx +0 -231
  260. package/src/player/components/useTimelineMarqueeSelection.ts +0 -276
  261. package/src/utils/timelineDiscovery.test.ts +0 -90
  262. /package/dist/{domEditingLayers-2ECJK24D.js.map → domEditingLayers-6LQGKPOI.js.map} +0 -0
@@ -5,39 +5,31 @@ import { remapBeatAnalysisToComposition } from "../../utils/beatEditActions";
5
5
  import { usePlayerStore, type TimelineElement } from "../store/playerStore";
6
6
  import { useExpandedTimelineElements } from "../hooks/useExpandedTimelineElements";
7
7
  import { useMountEffect } from "../../hooks/useMountEffect";
8
- import { EditPopover } from "./EditModal";
9
8
  import { defaultTimelineTheme } from "./timelineTheme";
10
9
  import { useTimelineRangeSelection } from "./useTimelineRangeSelection";
11
- import { useTimelineMarqueeSelection } from "./useTimelineMarqueeSelection";
12
10
  import { useTimelinePlayhead } from "./useTimelinePlayhead";
13
11
  import { useTimelineActiveClips } from "./useTimelineActiveClips";
14
12
  import { type TrackVisualStyle, getTrackStyle } from "./timelineIcons";
15
- import { getTimelinePixelsPerSecond } from "./timelineZoom";
16
13
  import { useTimelineZoom } from "./useTimelineZoom";
17
14
  import { useTimelineAssetDrop } from "./timelineDragDrop";
18
15
  import { TimelineEmptyState } from "./TimelineEmptyState";
19
16
  import { TimelineCanvas } from "./TimelineCanvas";
20
- import {
21
- KeyframeDiamondContextMenu,
22
- type KeyframeDiamondContextMenuState,
23
- } from "./KeyframeDiamondContextMenu";
17
+ import { type KeyframeDiamondContextMenuState } from "./KeyframeDiamondContextMenu";
24
18
  import { useTimelineClipDrag } from "./useTimelineClipDrag";
25
- import { useTimelineKeyframeHandlers } from "./useTimelineKeyframeHandlers";
26
- import { ClipContextMenu } from "./ClipContextMenu";
27
- import { TimelineShortcutHint } from "./TimelineShortcutHint";
28
- import { buildStackingTimelineLayers, insertPreviewTrackOrder } from "./timelineTrackOrder";
29
- import { getTimelineLayerGroupHeaderTotalHeight } from "./TimelineLayerGroupHeader";
19
+ import { TimelineOverlays } from "./TimelineOverlays";
20
+ import { useTimelineEditPinning } from "./useTimelineEditPinning";
21
+ import { useTimelineStackingSync } from "./useTimelineStackingSync";
22
+ import { useTimelineGeometry } from "./useTimelineGeometry";
30
23
  import {
31
24
  GUTTER,
32
- generateVisibleTicks,
25
+ generateTicks,
33
26
  getTimelineCanvasHeight,
34
27
  shouldShowTimelineShortcutHint,
35
- computeTimelineBasisDuration,
36
- computeTimelineEffectiveDuration,
37
28
  } from "./timelineLayout";
38
29
  import { useResolvedTimelineEditCallbacks } from "./useResolvedTimelineEditCallbacks";
39
30
  import type { TimelineProps } from "./TimelineTypes";
40
31
 
32
+ // Re-export pure utilities so existing imports from "./Timeline" still resolve.
41
33
  export {
42
34
  generateTicks,
43
35
  formatTimelineTickLabel,
@@ -62,7 +54,9 @@ export const Timeline = memo(function Timeline({
62
54
  onBlockDrop,
63
55
  onDeleteElement: _onDeleteElement,
64
56
  onMoveElement: onMoveElementOverride,
57
+ onMoveElements: onMoveElementsOverride,
65
58
  onResizeElement: onResizeElementOverride,
59
+ onResizeElements: onResizeElementsOverride,
66
60
  onBlockedEditAttempt: onBlockedEditAttemptOverride,
67
61
  onSplitElement: onSplitElementOverride,
68
62
  onSelectElement,
@@ -70,11 +64,9 @@ export const Timeline = memo(function Timeline({
70
64
  }: TimelineProps = {}) {
71
65
  const {
72
66
  onMoveElement,
73
- onResizeElement,
74
67
  onMoveElements,
68
+ onResizeElement,
75
69
  onResizeElements,
76
- onPreviewMoveElements,
77
- onPreviewResizeElements,
78
70
  onBlockedEditAttempt,
79
71
  onSplitElement,
80
72
  onRazorSplitAll,
@@ -85,7 +77,9 @@ export const Timeline = memo(function Timeline({
85
77
  onMoveKeyframe,
86
78
  } = useResolvedTimelineEditCallbacks({
87
79
  onMoveElement: onMoveElementOverride,
80
+ onMoveElements: onMoveElementsOverride,
88
81
  onResizeElement: onResizeElementOverride,
82
+ onResizeElements: onResizeElementsOverride,
89
83
  onBlockedEditAttempt: onBlockedEditAttemptOverride,
90
84
  onSplitElement: onSplitElementOverride,
91
85
  });
@@ -103,17 +97,20 @@ export const Timeline = memo(function Timeline({
103
97
  const duration = usePlayerStore((s) => s.duration);
104
98
  const timelineReady = usePlayerStore((s) => s.timelineReady);
105
99
  const selectedElementId = usePlayerStore((s) => s.selectedElementId);
100
+ const selectedElementIds = usePlayerStore((s) => s.selectedElementIds);
106
101
  const setSelectedElementId = usePlayerStore((s) => s.setSelectedElementId);
107
102
  const currentTime = usePlayerStore((s) => s.currentTime);
108
103
  const { zoomMode, manualZoomPercent, setZoomMode, setManualZoomPercent } = useTimelineZoom();
109
104
 
110
105
  const playheadRef = useRef<HTMLDivElement>(null);
106
+ const containerRef = useRef<HTMLDivElement>(null);
111
107
  const scrollRef = useRef<HTMLDivElement>(null);
112
108
  const activeTool = usePlayerStore((s) => s.activeTool);
113
109
  const [hoveredClip, setHoveredClip] = useState<string | null>(null);
114
110
  const isDragging = useRef(false);
115
111
  const [shiftHeld, setShiftHeld] = useState(false);
116
112
  const [razorGuideX, setRazorGuideX] = useState<number | null>(null);
113
+
117
114
  useMountEffect(() => {
118
115
  const down = (e: KeyboardEvent) => e.key === "Shift" && setShiftHeld(true);
119
116
  const up = (e: KeyboardEvent) => e.key === "Shift" && setShiftHeld(false);
@@ -139,6 +136,7 @@ export const Timeline = memo(function Timeline({
139
136
  const [viewportWidth, setViewportWidth] = useState(0);
140
137
  const roRef = useRef<ResizeObserver | null>(null);
141
138
  const shortcutHintRafRef = useRef(0);
139
+
142
140
  const syncShortcutHintVisibility = useCallback(() => {
143
141
  const scroll = scrollRef.current;
144
142
  setShowShortcutHint(
@@ -154,6 +152,16 @@ export const Timeline = memo(function Timeline({
154
152
  });
155
153
  }, [syncShortcutHintVisibility]);
156
154
 
155
+ const setContainerRef = useCallback((el: HTMLDivElement | null) => {
156
+ containerRef.current = el;
157
+ }, []);
158
+
159
+ // Last horizontal scroll offset, tracked so it can be RESTORED across the
160
+ // post-edit iframe reload: an edit re-derives elements (and may shrink the
161
+ // content width), which the browser clamps into a scroll jump. Paired with the
162
+ // pinned zoom (which keeps pps constant so the pixel offset stays meaningful),
163
+ // restoring this keeps the user parked at the same spot after any edit.
164
+ const lastScrollLeftRef = useRef(0);
157
165
  const setScrollRef = useCallback(
158
166
  (el: HTMLDivElement | null) => {
159
167
  if (roRef.current) {
@@ -180,31 +188,70 @@ export const Timeline = memo(function Timeline({
180
188
  if (shortcutHintRafRef.current) cancelAnimationFrame(shortcutHintRafRef.current);
181
189
  });
182
190
 
183
- const tracks = useMemo(
184
- () => buildStackingTimelineLayers(expandedElements).rows,
185
- [expandedElements],
186
- );
191
+ const effectiveDuration = useMemo(() => {
192
+ const safeDur = Number.isFinite(duration) ? duration : 0;
193
+ if (rawElements.length === 0) return safeDur;
194
+ const maxEnd = Math.max(...rawElements.map((el) => el.start + el.duration));
195
+ const result = Math.max(safeDur, maxEnd);
196
+ return Number.isFinite(result) ? result : safeDur;
197
+ }, [rawElements, duration]);
198
+
199
+ const tracks = useMemo(() => {
200
+ const map = new Map<number, typeof expandedElements>();
201
+ for (const el of expandedElements) {
202
+ const list = map.get(el.track) ?? [];
203
+ list.push(el);
204
+ map.set(el.track, list);
205
+ }
206
+ return Array.from(map.entries()).sort(([a], [b]) => a - b);
207
+ }, [expandedElements]);
187
208
 
188
209
  const trackStyles = useMemo(() => {
189
- const map = new Map<string, TrackVisualStyle>();
190
- for (const layer of tracks) {
191
- map.set(layer.id, getTrackStyle(layer.elements[0]?.tag ?? ""));
210
+ const map = new Map<number, TrackVisualStyle>();
211
+ for (const [trackNum, els] of tracks) {
212
+ map.set(trackNum, getTrackStyle(els[0]?.tag ?? ""));
192
213
  }
193
214
  return map;
194
215
  }, [tracks]);
195
216
 
196
- const trackOrder = useMemo(() => tracks.map((layer) => layer.id), [tracks]);
217
+ const trackOrder = useMemo(() => tracks.map(([trackNum]) => trackNum), [tracks]);
197
218
  const trackOrderRef = useRef(trackOrder);
198
219
  trackOrderRef.current = trackOrder;
199
- const timelineLayersRef = useRef(tracks);
200
- timelineLayersRef.current = tracks;
201
220
  const expandedElementsRef = useRef(expandedElements);
202
221
  expandedElementsRef.current = expandedElements;
203
222
 
204
223
  const ppsRef = useRef(100);
205
- const durationRef = useRef(Number.isFinite(duration) ? duration : 0);
224
+ const durationRef = useRef(effectiveDuration);
225
+ durationRef.current = effectiveDuration;
226
+ // Declared here (used before the fitPps derivation below) so the edit-pin
227
+ // wrappers can close over it; `fitPpsRef.current` is refreshed each render.
228
+ const fitPpsRef = useRef(100);
206
229
 
207
- const setRangeSelectionRef = useRef<((sel: null) => void) | null>(null);
230
+ const {
231
+ pinZoomBeforeEdit,
232
+ setRangeSelectionRef,
233
+ pinnedOnMoveElement,
234
+ pinnedOnMoveElements,
235
+ pinnedOnResizeElement,
236
+ pinnedOnResizeElements,
237
+ pinnedOnFileDrop,
238
+ pinnedOnAssetDrop,
239
+ pinnedOnBlockDrop,
240
+ } = useTimelineEditPinning({
241
+ ppsRef,
242
+ fitPpsRef,
243
+ onMoveElement,
244
+ onMoveElements,
245
+ onResizeElement,
246
+ onResizeElements,
247
+ onFileDrop,
248
+ onAssetDrop,
249
+ onBlockDrop,
250
+ });
251
+
252
+ const { readClipZIndex, applyStackingPatches, zSyncEnabled } = useTimelineStackingSync({
253
+ expandedElementsRef,
254
+ });
208
255
 
209
256
  const {
210
257
  draggedClip,
@@ -217,67 +264,39 @@ export const Timeline = memo(function Timeline({
217
264
  } = useTimelineClipDrag({
218
265
  scrollRef,
219
266
  ppsRef,
267
+ durationRef,
220
268
  trackOrderRef,
221
- timelineLayersRef,
222
- timelineElementsRef: expandedElementsRef,
223
- onMoveElement,
224
- onResizeElement,
225
- onMoveElements,
226
- onResizeElements,
227
- onPreviewMoveElements,
228
- onPreviewResizeElements,
269
+ onMoveElement: pinnedOnMoveElement,
270
+ onMoveElements: pinnedOnMoveElements,
271
+ onResizeElement: pinnedOnResizeElement,
272
+ onResizeElements: pinnedOnResizeElements,
229
273
  onBlockedEditAttempt,
230
274
  setShowPopover,
231
275
  setRangeSelectionRef,
276
+ readZIndex: zSyncEnabled ? readClipZIndex : undefined,
277
+ onStackingPatches: zSyncEnabled ? applyStackingPatches : undefined,
232
278
  });
233
279
 
234
- const basisDuration = useMemo(
235
- () =>
236
- computeTimelineBasisDuration(
237
- duration,
238
- rawElements.map((el) => el.start + el.duration),
239
- ),
240
- [rawElements, duration],
241
- );
242
- const effectiveDuration = useMemo(
243
- () =>
244
- computeTimelineEffectiveDuration(basisDuration, [
245
- draggedClip?.started ? draggedClip.previewStart + draggedClip.element.duration : null,
246
- resizingClip?.started ? resizingClip.previewStart + resizingClip.previewDuration : null,
247
- ]),
248
- [basisDuration, draggedClip, resizingClip],
249
- );
250
- durationRef.current = effectiveDuration;
280
+ const { isDragOver, handleAssetDragOver, handleAssetDrop, clearDropPreview } =
281
+ useTimelineAssetDrop({
282
+ scrollRef,
283
+ ppsRef,
284
+ durationRef,
285
+ trackOrderRef,
286
+ onFileDrop: pinnedOnFileDrop,
287
+ onAssetDrop: pinnedOnAssetDrop,
288
+ onBlockDrop: pinnedOnBlockDrop,
289
+ });
251
290
 
252
291
  const displayTrackOrder = useMemo(() => {
253
- if (
254
- !draggedClip?.started ||
255
- trackOrder.length === 0 ||
256
- trackOrder.includes(draggedClip.previewLayerId)
257
- )
258
- return trackOrder;
259
- return insertPreviewTrackOrder(
260
- trackOrder,
261
- draggedClip.previewLayerId,
262
- draggedClip.previewLayerIndex,
263
- );
292
+ if (!draggedClip?.started || trackOrder.includes(draggedClip.previewTrack)) return trackOrder;
293
+ return [...trackOrder, draggedClip.previewTrack].sort((a, b) => a - b);
264
294
  }, [draggedClip, trackOrder]);
265
295
 
266
- const totalH =
267
- getTimelineCanvasHeight(displayTrackOrder.length) +
268
- getTimelineLayerGroupHeaderTotalHeight(displayTrackOrder, tracks);
296
+ const totalH = getTimelineCanvasHeight(displayTrackOrder.length);
269
297
  const keyframeCache = usePlayerStore((s) => s.keyframeCache);
270
298
  const selectedKeyframes = usePlayerStore((s) => s.selectedKeyframes);
271
299
  const toggleSelectedKeyframe = usePlayerStore((s) => s.toggleSelectedKeyframe);
272
- const keyframeHandlers = useTimelineKeyframeHandlers({
273
- expandedElements,
274
- keyframeCache,
275
- onSelectElement,
276
- onSeek,
277
- setSelectedElementId,
278
- setKfContextMenu,
279
- toggleSelectedKeyframe,
280
- });
281
300
 
282
301
  const selectedElement = useMemo(
283
302
  () =>
@@ -287,26 +306,28 @@ export const Timeline = memo(function Timeline({
287
306
  const selectedElementRef = useRef<TimelineElement | null>(selectedElement);
288
307
  selectedElementRef.current = selectedElement;
289
308
 
290
- const fitPps =
291
- viewportWidth > GUTTER && basisDuration > 0
292
- ? (viewportWidth - GUTTER - 2) / basisDuration
293
- : 100;
294
- const pps = getTimelinePixelsPerSecond(fitPps, zoomMode, manualZoomPercent);
295
- ppsRef.current = pps;
296
- const trackContentWidth = Math.max(0, effectiveDuration * pps);
297
- const clipStateVersion = useMemo(
298
- () =>
299
- expandedElements
300
- .map((el) => `${el.key ?? el.id}:${el.start}:${el.duration}:${el.track}`)
301
- .join("|"),
302
- [expandedElements],
303
- );
304
- const zoomModeRef = useRef(zoomMode);
305
- zoomModeRef.current = zoomMode;
306
- const manualZoomPercentRef = useRef(manualZoomPercent);
307
- manualZoomPercentRef.current = manualZoomPercent;
308
- const fitPpsRef = useRef(fitPps);
309
- fitPpsRef.current = fitPps;
309
+ const {
310
+ pps,
311
+ fitPps,
312
+ displayContentWidth,
313
+ displayDuration,
314
+ clipStateVersion,
315
+ zoomModeRef,
316
+ manualZoomPercentRef,
317
+ } = useTimelineGeometry({
318
+ viewportWidth,
319
+ effectiveDuration,
320
+ zoomMode,
321
+ manualZoomPercent,
322
+ ppsRef,
323
+ fitPpsRef,
324
+ draggedClip,
325
+ resizingClip,
326
+ expandedElements,
327
+ isDragging,
328
+ scrollRef,
329
+ lastScrollLeftRef,
330
+ });
310
331
 
311
332
  const { seekFromX, autoScrollDuringDrag, dragScrollRaf } = useTimelinePlayhead({
312
333
  playheadRef,
@@ -339,6 +360,8 @@ export const Timeline = memo(function Timeline({
339
360
  rangeSelection,
340
361
  setRangeSelection,
341
362
  shiftClickClipRef,
363
+ marqueeRect,
364
+ isScrubbing,
342
365
  handlePointerDown,
343
366
  handlePointerMove,
344
367
  handlePointerUp,
@@ -353,28 +376,12 @@ export const Timeline = memo(function Timeline({
353
376
  dragScrollRaf,
354
377
  isDragging,
355
378
  setShowPopover,
356
- });
357
- const {
358
- marqueeRect,
359
- handlePointerDown: handleMarqueePointerDown,
360
- handlePointerMove: handleMarqueePointerMove,
361
- handlePointerUp: handleMarqueePointerUp,
362
- } = useTimelineMarqueeSelection({
363
- scrollRef,
364
- ppsRef,
379
+ elementsRef: expandedElementsRef,
365
380
  trackOrderRef,
366
- timelineLayersRef,
367
- disabled: activeTool === "razor",
368
- setShowPopover,
369
- setRangeSelectionRef,
370
- seekFromX,
381
+ onSelectElement,
371
382
  });
383
+ // Wire setRangeSelection into the stable ref consumed by useTimelineClipDrag
372
384
  setRangeSelectionRef.current = setRangeSelection;
373
- // Pointer-up and lost-capture end a gesture identically (marquee-claims-first).
374
- const releasePointer = (event: Parameters<typeof handleMarqueePointerUp>[0]) => {
375
- if (handleMarqueePointerUp(event)) return;
376
- handlePointerUp();
377
- };
378
385
 
379
386
  const prevSelectedRef = useRef(selectedElementRef.current);
380
387
  // eslint-disable-next-line no-restricted-syntax, react-hooks/exhaustive-deps
@@ -389,8 +396,8 @@ export const Timeline = memo(function Timeline({
389
396
  });
390
397
 
391
398
  const { major, minor } = useMemo(
392
- () => generateVisibleTicks(effectiveDuration, pps, viewportWidth, GUTTER),
393
- [effectiveDuration, pps, viewportWidth],
399
+ () => generateTicks(displayDuration, pps),
400
+ [displayDuration, pps],
394
401
  );
395
402
  const majorTickInterval = major.length >= 2 ? major[1] - major[0] : effectiveDuration;
396
403
 
@@ -416,24 +423,13 @@ export const Timeline = memo(function Timeline({
416
423
  [resizingClip],
417
424
  );
418
425
 
419
- const { isDragOver, setIsDragOver, handleAssetDragOver, handleAssetDrop } = useTimelineAssetDrop({
420
- scrollRef,
421
- ppsRef,
422
- durationRef,
423
- trackOrderRef,
424
- timelineLayersRef,
425
- onFileDrop,
426
- onAssetDrop,
427
- onBlockDrop,
428
- });
429
-
430
426
  if (!timelineReady || expandedElements.length === 0) {
431
427
  return (
432
428
  <TimelineEmptyState
433
429
  isDragOver={isDragOver}
434
430
  onFileDrop={!!onFileDrop}
435
431
  onDragOver={handleAssetDragOver}
436
- onDragLeave={() => setIsDragOver(false)}
432
+ onDragLeave={() => clearDropPreview()}
437
433
  onDrop={handleAssetDrop}
438
434
  />
439
435
  );
@@ -441,6 +437,7 @@ export const Timeline = memo(function Timeline({
441
437
 
442
438
  return (
443
439
  <div
440
+ ref={setContainerRef}
444
441
  aria-label="Timeline"
445
442
  className={`relative border-t select-none h-full overflow-hidden ${activeTool === "razor" ? "cursor-crosshair" : shiftHeld ? "cursor-crosshair" : "cursor-default"}`}
446
443
  onMouseMove={(e) => {
@@ -460,8 +457,12 @@ export const Timeline = memo(function Timeline({
460
457
  ref={setScrollRef}
461
458
  tabIndex={-1}
462
459
  className={`${zoomMode === "fit" ? "overflow-x-hidden" : "overflow-x-auto"} overflow-y-auto h-full outline-none`}
460
+ onScroll={(e) => {
461
+ // Remember the live offset so it can be restored across a post-edit reload.
462
+ lastScrollLeftRef.current = e.currentTarget.scrollLeft;
463
+ }}
463
464
  onDragOver={handleAssetDragOver}
464
- onDragLeave={() => setIsDragOver(false)}
465
+ onDragLeave={() => clearDropPreview()}
465
466
  onDrop={handleAssetDrop}
466
467
  onPointerDown={(e) => {
467
468
  if (activeTool === "razor" && e.shiftKey && e.button === 0 && scrollRef.current) {
@@ -471,21 +472,17 @@ export const Timeline = memo(function Timeline({
471
472
  onRazorSplitAll?.(splitTime);
472
473
  return;
473
474
  }
474
- if (handleMarqueePointerDown(e)) return;
475
475
  handlePointerDown(e);
476
476
  }}
477
- onPointerMove={(e) => {
478
- if (handleMarqueePointerMove(e)) return;
479
- handlePointerMove(e);
480
- }}
481
- onPointerUp={releasePointer}
482
- onLostPointerCapture={releasePointer}
477
+ onPointerMove={handlePointerMove}
478
+ onPointerUp={handlePointerUp}
479
+ onLostPointerCapture={handlePointerUp}
483
480
  >
484
481
  <TimelineCanvas
485
482
  major={major}
486
483
  minor={minor}
487
484
  pps={pps}
488
- trackContentWidth={trackContentWidth}
485
+ trackContentWidth={displayContentWidth}
489
486
  totalH={totalH}
490
487
  effectiveDuration={effectiveDuration}
491
488
  majorTickInterval={majorTickInterval}
@@ -497,9 +494,11 @@ export const Timeline = memo(function Timeline({
497
494
  tracks={tracks}
498
495
  trackStyles={trackStyles}
499
496
  selectedElementId={selectedElementId}
497
+ selectedElementIds={selectedElementIds}
500
498
  hoveredClip={hoveredClip}
501
499
  draggedClip={draggedClip}
502
500
  resizingClip={resizingClip}
501
+ isScrubbing={isScrubbing}
503
502
  blockedClipRef={blockedClipRef}
504
503
  suppressClickRef={suppressClickRef}
505
504
  scrollRef={scrollRef}
@@ -522,10 +521,41 @@ export const Timeline = memo(function Timeline({
522
521
  selectedKeyframes={selectedKeyframes}
523
522
  currentTime={currentTime}
524
523
  beatAnalysis={adjustedBeatAnalysis}
525
- onClickKeyframe={keyframeHandlers.onClickKeyframe}
526
- onShiftClickKeyframe={keyframeHandlers.onShiftClickKeyframe}
524
+ onClickKeyframe={(el, pct) => {
525
+ usePlayerStore.getState().clearSelectedKeyframes();
526
+ const elKey = el.key ?? el.id;
527
+ setSelectedElementId(elKey);
528
+ onSelectElement?.(el);
529
+ // Visually select the clicked diamond (matches shift-click / motion-path
530
+ // selection); cleared above so this single-selects it.
531
+ toggleSelectedKeyframe(`${elKey}:${pct}`);
532
+ const absTime = el.start + (pct / 100) * el.duration;
533
+ onSeek?.(absTime);
534
+ const kfData = keyframeCache?.get(elKey);
535
+ const kf = kfData?.keyframes.find((k) => Math.abs(k.percentage - pct) < 0.5);
536
+ usePlayerStore.getState().setActiveKeyframePct(kf?.tweenPercentage ?? null);
537
+ }}
538
+ onShiftClickKeyframe={(elId, pct) => {
539
+ toggleSelectedKeyframe(`${elId}:${pct}`);
540
+ }}
527
541
  onMoveKeyframe={onMoveKeyframe}
528
- onContextMenuKeyframe={keyframeHandlers.onContextMenuKeyframe}
542
+ onContextMenuKeyframe={(e, elId, pct) => {
543
+ const el = expandedElements.find((x) => (x.key ?? x.id) === elId);
544
+ if (el) {
545
+ setSelectedElementId(elId);
546
+ onSelectElement?.(el);
547
+ }
548
+ const kfData = keyframeCache.get(elId);
549
+ const kf = kfData?.keyframes.find((k) => Math.abs(k.percentage - pct) < 0.2);
550
+ setKfContextMenu({
551
+ x: e.clientX + 4,
552
+ y: e.clientY + 2,
553
+ elementId: elId,
554
+ percentage: pct,
555
+ tweenPercentage: kf?.tweenPercentage,
556
+ currentEase: kf?.ease ?? kfData?.ease,
557
+ });
558
+ }}
529
559
  onContextMenuClip={(e, el) => {
530
560
  e.preventDefault();
531
561
  setSelectedElementId(el.key ?? el.id);
@@ -544,57 +574,27 @@ export const Timeline = memo(function Timeline({
544
574
  />
545
575
  )}
546
576
  </div>
547
-
548
- {showShortcutHint && !showPopover && !rangeSelection && (
549
- <TimelineShortcutHint theme={theme} />
550
- )}
551
-
552
- {showPopover && rangeSelection && (
553
- <EditPopover
554
- rangeStart={rangeSelection.start}
555
- rangeEnd={rangeSelection.end}
556
- anchorX={rangeSelection.anchorX}
557
- anchorY={rangeSelection.anchorY}
558
- onClose={() => {
559
- setShowPopover(false);
560
- setRangeSelection(null);
561
- }}
562
- />
563
- )}
564
-
565
- {kfContextMenu && (
566
- <KeyframeDiamondContextMenu
567
- state={kfContextMenu}
568
- onClose={() => setKfContextMenu(null)}
569
- onDelete={(elId, pct) => onDeleteKeyframe?.(elId, pct)}
570
- onDeleteAll={(elId) => onDeleteAllKeyframes?.(elId)}
571
- onChangeEase={(elId, pct, ease) => onChangeKeyframeEase?.(elId, pct, ease)}
572
- onMoveToPlayhead={
573
- onMoveKeyframeToPlayhead
574
- ? (elId, pct) => onMoveKeyframeToPlayhead(elId, pct)
575
- : undefined
576
- }
577
- onCopyProperties={(elId, pct) => {
578
- const kfData = keyframeCache.get(elId);
579
- const kf = kfData?.keyframes.find((k) => k.percentage === pct);
580
- if (kf) {
581
- void navigator.clipboard.writeText(JSON.stringify(kf.properties, null, 2));
582
- }
583
- }}
584
- />
585
- )}
586
-
587
- {clipContextMenu && (
588
- <ClipContextMenu
589
- x={clipContextMenu.x}
590
- y={clipContextMenu.y}
591
- element={clipContextMenu.element}
592
- currentTime={currentTime}
593
- onClose={() => setClipContextMenu(null)}
594
- onSplit={(el, time) => onSplitElement?.(el, time)}
595
- onDelete={(el) => _onDeleteElement?.(el)}
596
- />
597
- )}
577
+ <TimelineOverlays
578
+ theme={theme}
579
+ showShortcutHint={showShortcutHint}
580
+ showPopover={showPopover}
581
+ rangeSelection={rangeSelection}
582
+ setShowPopover={setShowPopover}
583
+ setRangeSelection={setRangeSelection}
584
+ kfContextMenu={kfContextMenu}
585
+ setKfContextMenu={setKfContextMenu}
586
+ onDeleteKeyframe={onDeleteKeyframe}
587
+ onDeleteAllKeyframes={onDeleteAllKeyframes}
588
+ onChangeKeyframeEase={onChangeKeyframeEase}
589
+ onMoveKeyframeToPlayhead={onMoveKeyframeToPlayhead}
590
+ keyframeCache={keyframeCache}
591
+ clipContextMenu={clipContextMenu}
592
+ setClipContextMenu={setClipContextMenu}
593
+ currentTime={currentTime}
594
+ onSplitElement={onSplitElement}
595
+ pinZoomBeforeEdit={pinZoomBeforeEdit}
596
+ onDeleteElement={_onDeleteElement}
597
+ />
598
598
  </div>
599
599
  );
600
600
  });