@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
@@ -5,39 +5,32 @@ 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";
29
+ import { STUDIO_PREVIEW_FPS } from "../lib/time";
38
30
  import { useResolvedTimelineEditCallbacks } from "./useResolvedTimelineEditCallbacks";
39
31
  import type { TimelineProps } from "./TimelineTypes";
40
32
 
33
+ // Re-export pure utilities so existing imports from "./Timeline" still resolve.
41
34
  export {
42
35
  generateTicks,
43
36
  formatTimelineTickLabel,
@@ -62,7 +55,9 @@ export const Timeline = memo(function Timeline({
62
55
  onBlockDrop,
63
56
  onDeleteElement: _onDeleteElement,
64
57
  onMoveElement: onMoveElementOverride,
58
+ onMoveElements: onMoveElementsOverride,
65
59
  onResizeElement: onResizeElementOverride,
60
+ onResizeElements: onResizeElementsOverride,
66
61
  onBlockedEditAttempt: onBlockedEditAttemptOverride,
67
62
  onSplitElement: onSplitElementOverride,
68
63
  onSelectElement,
@@ -70,11 +65,9 @@ export const Timeline = memo(function Timeline({
70
65
  }: TimelineProps = {}) {
71
66
  const {
72
67
  onMoveElement,
73
- onResizeElement,
74
68
  onMoveElements,
69
+ onResizeElement,
75
70
  onResizeElements,
76
- onPreviewMoveElements,
77
- onPreviewResizeElements,
78
71
  onBlockedEditAttempt,
79
72
  onSplitElement,
80
73
  onRazorSplitAll,
@@ -85,7 +78,9 @@ export const Timeline = memo(function Timeline({
85
78
  onMoveKeyframe,
86
79
  } = useResolvedTimelineEditCallbacks({
87
80
  onMoveElement: onMoveElementOverride,
81
+ onMoveElements: onMoveElementsOverride,
88
82
  onResizeElement: onResizeElementOverride,
83
+ onResizeElements: onResizeElementsOverride,
89
84
  onBlockedEditAttempt: onBlockedEditAttemptOverride,
90
85
  onSplitElement: onSplitElementOverride,
91
86
  });
@@ -101,19 +96,23 @@ export const Timeline = memo(function Timeline({
101
96
  [beatAnalysis, musicElement, beatEdits],
102
97
  );
103
98
  const duration = usePlayerStore((s) => s.duration);
99
+ const timeDisplayMode = usePlayerStore((s) => s.timeDisplayMode);
104
100
  const timelineReady = usePlayerStore((s) => s.timelineReady);
105
101
  const selectedElementId = usePlayerStore((s) => s.selectedElementId);
102
+ const selectedElementIds = usePlayerStore((s) => s.selectedElementIds);
106
103
  const setSelectedElementId = usePlayerStore((s) => s.setSelectedElementId);
107
104
  const currentTime = usePlayerStore((s) => s.currentTime);
108
105
  const { zoomMode, manualZoomPercent, setZoomMode, setManualZoomPercent } = useTimelineZoom();
109
106
 
110
107
  const playheadRef = useRef<HTMLDivElement>(null);
108
+ const containerRef = useRef<HTMLDivElement>(null);
111
109
  const scrollRef = useRef<HTMLDivElement>(null);
112
110
  const activeTool = usePlayerStore((s) => s.activeTool);
113
111
  const [hoveredClip, setHoveredClip] = useState<string | null>(null);
114
112
  const isDragging = useRef(false);
115
113
  const [shiftHeld, setShiftHeld] = useState(false);
116
114
  const [razorGuideX, setRazorGuideX] = useState<number | null>(null);
115
+
117
116
  useMountEffect(() => {
118
117
  const down = (e: KeyboardEvent) => e.key === "Shift" && setShiftHeld(true);
119
118
  const up = (e: KeyboardEvent) => e.key === "Shift" && setShiftHeld(false);
@@ -139,6 +138,7 @@ export const Timeline = memo(function Timeline({
139
138
  const [viewportWidth, setViewportWidth] = useState(0);
140
139
  const roRef = useRef<ResizeObserver | null>(null);
141
140
  const shortcutHintRafRef = useRef(0);
141
+
142
142
  const syncShortcutHintVisibility = useCallback(() => {
143
143
  const scroll = scrollRef.current;
144
144
  setShowShortcutHint(
@@ -154,6 +154,13 @@ export const Timeline = memo(function Timeline({
154
154
  });
155
155
  }, [syncShortcutHintVisibility]);
156
156
 
157
+ const setContainerRef = useCallback((el: HTMLDivElement | null) => {
158
+ containerRef.current = el;
159
+ }, []);
160
+
161
+ // Last horizontal scroll offset, RESTORED across the post-edit iframe reload (which clamps into
162
+ // a scroll jump); with the pinned zoom this keeps the user parked at the same spot after edits.
163
+ const lastScrollLeftRef = useRef(0);
157
164
  const setScrollRef = useCallback(
158
165
  (el: HTMLDivElement | null) => {
159
166
  if (roRef.current) {
@@ -180,31 +187,70 @@ export const Timeline = memo(function Timeline({
180
187
  if (shortcutHintRafRef.current) cancelAnimationFrame(shortcutHintRafRef.current);
181
188
  });
182
189
 
183
- const tracks = useMemo(
184
- () => buildStackingTimelineLayers(expandedElements).rows,
185
- [expandedElements],
186
- );
190
+ const effectiveDuration = useMemo(() => {
191
+ const safeDur = Number.isFinite(duration) ? duration : 0;
192
+ if (rawElements.length === 0) return safeDur;
193
+ const maxEnd = Math.max(...rawElements.map((el) => el.start + el.duration));
194
+ const result = Math.max(safeDur, maxEnd);
195
+ return Number.isFinite(result) ? result : safeDur;
196
+ }, [rawElements, duration]);
197
+
198
+ const tracks = useMemo(() => {
199
+ const map = new Map<number, typeof expandedElements>();
200
+ for (const el of expandedElements) {
201
+ const list = map.get(el.track) ?? [];
202
+ list.push(el);
203
+ map.set(el.track, list);
204
+ }
205
+ return Array.from(map.entries()).sort(([a], [b]) => a - b);
206
+ }, [expandedElements]);
187
207
 
188
208
  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 ?? ""));
209
+ const map = new Map<number, TrackVisualStyle>();
210
+ for (const [trackNum, els] of tracks) {
211
+ map.set(trackNum, getTrackStyle(els[0]?.tag ?? ""));
192
212
  }
193
213
  return map;
194
214
  }, [tracks]);
195
215
 
196
- const trackOrder = useMemo(() => tracks.map((layer) => layer.id), [tracks]);
216
+ const trackOrder = useMemo(() => tracks.map(([trackNum]) => trackNum), [tracks]);
197
217
  const trackOrderRef = useRef(trackOrder);
198
218
  trackOrderRef.current = trackOrder;
199
- const timelineLayersRef = useRef(tracks);
200
- timelineLayersRef.current = tracks;
201
219
  const expandedElementsRef = useRef(expandedElements);
202
220
  expandedElementsRef.current = expandedElements;
203
221
 
204
222
  const ppsRef = useRef(100);
205
- const durationRef = useRef(Number.isFinite(duration) ? duration : 0);
223
+ const durationRef = useRef(effectiveDuration);
224
+ durationRef.current = effectiveDuration;
225
+ // Declared here (used before the fitPps derivation below) so the edit-pin
226
+ // wrappers can close over it; `fitPpsRef.current` is refreshed each render.
227
+ const fitPpsRef = useRef(100);
206
228
 
207
- const setRangeSelectionRef = useRef<((sel: null) => void) | null>(null);
229
+ const {
230
+ pinZoomBeforeEdit,
231
+ setRangeSelectionRef,
232
+ pinnedOnMoveElement,
233
+ pinnedOnMoveElements,
234
+ pinnedOnResizeElement,
235
+ pinnedOnResizeElements,
236
+ pinnedOnFileDrop,
237
+ pinnedOnAssetDrop,
238
+ pinnedOnBlockDrop,
239
+ } = useTimelineEditPinning({
240
+ ppsRef,
241
+ fitPpsRef,
242
+ onMoveElement,
243
+ onMoveElements,
244
+ onResizeElement,
245
+ onResizeElements,
246
+ onFileDrop,
247
+ onAssetDrop,
248
+ onBlockDrop,
249
+ });
250
+
251
+ const { readClipZIndex, applyStackingPatches, zSyncEnabled } = useTimelineStackingSync({
252
+ expandedElementsRef,
253
+ });
208
254
 
209
255
  const {
210
256
  draggedClip,
@@ -217,67 +263,39 @@ export const Timeline = memo(function Timeline({
217
263
  } = useTimelineClipDrag({
218
264
  scrollRef,
219
265
  ppsRef,
266
+ durationRef,
220
267
  trackOrderRef,
221
- timelineLayersRef,
222
- timelineElementsRef: expandedElementsRef,
223
- onMoveElement,
224
- onResizeElement,
225
- onMoveElements,
226
- onResizeElements,
227
- onPreviewMoveElements,
228
- onPreviewResizeElements,
268
+ onMoveElement: pinnedOnMoveElement,
269
+ onMoveElements: pinnedOnMoveElements,
270
+ onResizeElement: pinnedOnResizeElement,
271
+ onResizeElements: pinnedOnResizeElements,
229
272
  onBlockedEditAttempt,
230
273
  setShowPopover,
231
274
  setRangeSelectionRef,
275
+ readZIndex: zSyncEnabled ? readClipZIndex : undefined,
276
+ onStackingPatches: zSyncEnabled ? applyStackingPatches : undefined,
232
277
  });
233
278
 
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;
279
+ const { isDragOver, handleAssetDragOver, handleAssetDrop, clearDropPreview } =
280
+ useTimelineAssetDrop({
281
+ scrollRef,
282
+ ppsRef,
283
+ durationRef,
284
+ trackOrderRef,
285
+ onFileDrop: pinnedOnFileDrop,
286
+ onAssetDrop: pinnedOnAssetDrop,
287
+ onBlockDrop: pinnedOnBlockDrop,
288
+ });
251
289
 
252
290
  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
- );
291
+ if (!draggedClip?.started || trackOrder.includes(draggedClip.previewTrack)) return trackOrder;
292
+ return [...trackOrder, draggedClip.previewTrack].sort((a, b) => a - b);
264
293
  }, [draggedClip, trackOrder]);
265
294
 
266
- const totalH =
267
- getTimelineCanvasHeight(displayTrackOrder.length) +
268
- getTimelineLayerGroupHeaderTotalHeight(displayTrackOrder, tracks);
295
+ const totalH = getTimelineCanvasHeight(displayTrackOrder.length);
269
296
  const keyframeCache = usePlayerStore((s) => s.keyframeCache);
270
297
  const selectedKeyframes = usePlayerStore((s) => s.selectedKeyframes);
271
298
  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
299
 
282
300
  const selectedElement = useMemo(
283
301
  () =>
@@ -287,26 +305,28 @@ export const Timeline = memo(function Timeline({
287
305
  const selectedElementRef = useRef<TimelineElement | null>(selectedElement);
288
306
  selectedElementRef.current = selectedElement;
289
307
 
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;
308
+ const {
309
+ pps,
310
+ fitPps,
311
+ displayContentWidth,
312
+ displayDuration,
313
+ clipStateVersion,
314
+ zoomModeRef,
315
+ manualZoomPercentRef,
316
+ } = useTimelineGeometry({
317
+ viewportWidth,
318
+ effectiveDuration,
319
+ zoomMode,
320
+ manualZoomPercent,
321
+ ppsRef,
322
+ fitPpsRef,
323
+ draggedClip,
324
+ resizingClip,
325
+ expandedElements,
326
+ isDragging,
327
+ scrollRef,
328
+ lastScrollLeftRef,
329
+ });
310
330
 
311
331
  const { seekFromX, autoScrollDuringDrag, dragScrollRaf } = useTimelinePlayhead({
312
332
  playheadRef,
@@ -339,6 +359,8 @@ export const Timeline = memo(function Timeline({
339
359
  rangeSelection,
340
360
  setRangeSelection,
341
361
  shiftClickClipRef,
362
+ marqueeRect,
363
+ isScrubbing,
342
364
  handlePointerDown,
343
365
  handlePointerMove,
344
366
  handlePointerUp,
@@ -353,28 +375,12 @@ export const Timeline = memo(function Timeline({
353
375
  dragScrollRaf,
354
376
  isDragging,
355
377
  setShowPopover,
356
- });
357
- const {
358
- marqueeRect,
359
- handlePointerDown: handleMarqueePointerDown,
360
- handlePointerMove: handleMarqueePointerMove,
361
- handlePointerUp: handleMarqueePointerUp,
362
- } = useTimelineMarqueeSelection({
363
- scrollRef,
364
- ppsRef,
378
+ elementsRef: expandedElementsRef,
365
379
  trackOrderRef,
366
- timelineLayersRef,
367
- disabled: activeTool === "razor",
368
- setShowPopover,
369
- setRangeSelectionRef,
370
- seekFromX,
380
+ onSelectElement,
371
381
  });
382
+ // Wire setRangeSelection into the stable ref consumed by useTimelineClipDrag
372
383
  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
384
 
379
385
  const prevSelectedRef = useRef(selectedElementRef.current);
380
386
  // eslint-disable-next-line no-restricted-syntax, react-hooks/exhaustive-deps
@@ -388,9 +394,11 @@ export const Timeline = memo(function Timeline({
388
394
  }
389
395
  });
390
396
 
397
+ // Frame display mode labels ruler ticks as frame numbers — pass the fps so ticks snap to frames.
398
+ const tickFps = timeDisplayMode === "frame" ? STUDIO_PREVIEW_FPS : undefined;
391
399
  const { major, minor } = useMemo(
392
- () => generateVisibleTicks(effectiveDuration, pps, viewportWidth, GUTTER),
393
- [effectiveDuration, pps, viewportWidth],
400
+ () => generateTicks(displayDuration, pps, tickFps),
401
+ [displayDuration, pps, tickFps],
394
402
  );
395
403
  const majorTickInterval = major.length >= 2 ? major[1] - major[0] : effectiveDuration;
396
404
 
@@ -416,24 +424,13 @@ export const Timeline = memo(function Timeline({
416
424
  [resizingClip],
417
425
  );
418
426
 
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
427
  if (!timelineReady || expandedElements.length === 0) {
431
428
  return (
432
429
  <TimelineEmptyState
433
430
  isDragOver={isDragOver}
434
431
  onFileDrop={!!onFileDrop}
435
432
  onDragOver={handleAssetDragOver}
436
- onDragLeave={() => setIsDragOver(false)}
433
+ onDragLeave={() => clearDropPreview()}
437
434
  onDrop={handleAssetDrop}
438
435
  />
439
436
  );
@@ -441,6 +438,7 @@ export const Timeline = memo(function Timeline({
441
438
 
442
439
  return (
443
440
  <div
441
+ ref={setContainerRef}
444
442
  aria-label="Timeline"
445
443
  className={`relative border-t select-none h-full overflow-hidden ${activeTool === "razor" ? "cursor-crosshair" : shiftHeld ? "cursor-crosshair" : "cursor-default"}`}
446
444
  onMouseMove={(e) => {
@@ -460,8 +458,12 @@ export const Timeline = memo(function Timeline({
460
458
  ref={setScrollRef}
461
459
  tabIndex={-1}
462
460
  className={`${zoomMode === "fit" ? "overflow-x-hidden" : "overflow-x-auto"} overflow-y-auto h-full outline-none`}
461
+ onScroll={(e) => {
462
+ // Remember the live offset so it can be restored across a post-edit reload.
463
+ lastScrollLeftRef.current = e.currentTarget.scrollLeft;
464
+ }}
463
465
  onDragOver={handleAssetDragOver}
464
- onDragLeave={() => setIsDragOver(false)}
466
+ onDragLeave={() => clearDropPreview()}
465
467
  onDrop={handleAssetDrop}
466
468
  onPointerDown={(e) => {
467
469
  if (activeTool === "razor" && e.shiftKey && e.button === 0 && scrollRef.current) {
@@ -471,21 +473,17 @@ export const Timeline = memo(function Timeline({
471
473
  onRazorSplitAll?.(splitTime);
472
474
  return;
473
475
  }
474
- if (handleMarqueePointerDown(e)) return;
475
476
  handlePointerDown(e);
476
477
  }}
477
- onPointerMove={(e) => {
478
- if (handleMarqueePointerMove(e)) return;
479
- handlePointerMove(e);
480
- }}
481
- onPointerUp={releasePointer}
482
- onLostPointerCapture={releasePointer}
478
+ onPointerMove={handlePointerMove}
479
+ onPointerUp={handlePointerUp}
480
+ onLostPointerCapture={handlePointerUp}
483
481
  >
484
482
  <TimelineCanvas
485
483
  major={major}
486
484
  minor={minor}
487
485
  pps={pps}
488
- trackContentWidth={trackContentWidth}
486
+ trackContentWidth={displayContentWidth}
489
487
  totalH={totalH}
490
488
  effectiveDuration={effectiveDuration}
491
489
  majorTickInterval={majorTickInterval}
@@ -497,9 +495,11 @@ export const Timeline = memo(function Timeline({
497
495
  tracks={tracks}
498
496
  trackStyles={trackStyles}
499
497
  selectedElementId={selectedElementId}
498
+ selectedElementIds={selectedElementIds}
500
499
  hoveredClip={hoveredClip}
501
500
  draggedClip={draggedClip}
502
501
  resizingClip={resizingClip}
502
+ isScrubbing={isScrubbing}
503
503
  blockedClipRef={blockedClipRef}
504
504
  suppressClickRef={suppressClickRef}
505
505
  scrollRef={scrollRef}
@@ -522,10 +522,40 @@ export const Timeline = memo(function Timeline({
522
522
  selectedKeyframes={selectedKeyframes}
523
523
  currentTime={currentTime}
524
524
  beatAnalysis={adjustedBeatAnalysis}
525
- onClickKeyframe={keyframeHandlers.onClickKeyframe}
526
- onShiftClickKeyframe={keyframeHandlers.onShiftClickKeyframe}
525
+ onClickKeyframe={(el, pct) => {
526
+ usePlayerStore.getState().clearSelectedKeyframes();
527
+ const elKey = el.key ?? el.id;
528
+ setSelectedElementId(elKey);
529
+ onSelectElement?.(el);
530
+ // Select the clicked diamond (matches shift-click); cleared above so this single-selects.
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
  });