@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
@@ -0,0 +1,121 @@
1
+ import { useCallback, useMemo, useRef, type RefObject } from "react";
2
+ import { usePlayerStore } from "../store/playerStore";
3
+ import type { TimelineDropCallbacks, TimelineEditCallbacks } from "./timelineCallbacks";
4
+
5
+ interface UseTimelineEditPinningInput {
6
+ ppsRef: RefObject<number>;
7
+ fitPpsRef: RefObject<number>;
8
+ onMoveElement: TimelineEditCallbacks["onMoveElement"];
9
+ onMoveElements: TimelineEditCallbacks["onMoveElements"];
10
+ onResizeElement: TimelineEditCallbacks["onResizeElement"];
11
+ onResizeElements: TimelineEditCallbacks["onResizeElements"];
12
+ onFileDrop: TimelineDropCallbacks["onFileDrop"];
13
+ onAssetDrop: TimelineDropCallbacks["onAssetDrop"];
14
+ onBlockDrop: TimelineDropCallbacks["onBlockDrop"];
15
+ }
16
+
17
+ // Wrap every mutating timeline edit so the zoom pins to the current on-screen
18
+ // scale right before the edit commits — the reload the edit triggers then keeps
19
+ // the current scale instead of rescaling every clip (the blink-fix symptom).
20
+ // Each wrapper forwards its args unchanged and preserves the original's absence
21
+ // (an unset callback stays unset → the timeline's own fallbacks kick in).
22
+ export function useTimelineEditPinning({
23
+ ppsRef,
24
+ fitPpsRef,
25
+ onMoveElement,
26
+ onMoveElements,
27
+ onResizeElement,
28
+ onResizeElements,
29
+ onFileDrop,
30
+ onAssetDrop,
31
+ onBlockDrop,
32
+ }: UseTimelineEditPinningInput) {
33
+ const pinTimelineZoom = usePlayerStore((s) => s.pinTimelineZoom);
34
+ // Pin the timeline zoom to the current on-screen scale on the FIRST edit, so a
35
+ // duration change from that edit (drops/moves/deletes recompute the fit basis)
36
+ // stops rescaling every clip. `pinTimelineZoom` is a no-op once already pinned
37
+ // (or after a manual zoom), so the user's own zoom is never clobbered; Fit
38
+ // re-fits. Reads refs at call time for the latest scale.
39
+ const pinZoomBeforeEdit = useCallback(() => {
40
+ pinTimelineZoom(ppsRef.current, fitPpsRef.current);
41
+ }, [pinTimelineZoom, ppsRef, fitPpsRef]);
42
+
43
+ // Stable ref so useTimelineClipDrag can clear rangeSelection without circular dep
44
+ const setRangeSelectionRef = useRef<((sel: null) => void) | null>(null);
45
+
46
+ const pinnedOnMoveElement = useMemo(
47
+ () =>
48
+ onMoveElement &&
49
+ ((...args: Parameters<typeof onMoveElement>) => {
50
+ pinZoomBeforeEdit();
51
+ return onMoveElement(...args);
52
+ }),
53
+ [onMoveElement, pinZoomBeforeEdit],
54
+ );
55
+ const pinnedOnMoveElements = useMemo(
56
+ () =>
57
+ onMoveElements &&
58
+ ((...args: Parameters<typeof onMoveElements>) => {
59
+ pinZoomBeforeEdit();
60
+ return onMoveElements(...args);
61
+ }),
62
+ [onMoveElements, pinZoomBeforeEdit],
63
+ );
64
+ const pinnedOnResizeElement = useMemo(
65
+ () =>
66
+ onResizeElement &&
67
+ ((...args: Parameters<typeof onResizeElement>) => {
68
+ pinZoomBeforeEdit();
69
+ return onResizeElement(...args);
70
+ }),
71
+ [onResizeElement, pinZoomBeforeEdit],
72
+ );
73
+ const pinnedOnResizeElements = useMemo(
74
+ () =>
75
+ onResizeElements &&
76
+ ((...args: Parameters<typeof onResizeElements>) => {
77
+ pinZoomBeforeEdit();
78
+ return onResizeElements(...args);
79
+ }),
80
+ [onResizeElements, pinZoomBeforeEdit],
81
+ );
82
+ const pinnedOnFileDrop = useMemo(
83
+ () =>
84
+ onFileDrop &&
85
+ ((...args: Parameters<typeof onFileDrop>) => {
86
+ pinZoomBeforeEdit();
87
+ return onFileDrop(...args);
88
+ }),
89
+ [onFileDrop, pinZoomBeforeEdit],
90
+ );
91
+ const pinnedOnAssetDrop = useMemo(
92
+ () =>
93
+ onAssetDrop &&
94
+ ((...args: Parameters<typeof onAssetDrop>) => {
95
+ pinZoomBeforeEdit();
96
+ return onAssetDrop(...args);
97
+ }),
98
+ [onAssetDrop, pinZoomBeforeEdit],
99
+ );
100
+ const pinnedOnBlockDrop = useMemo(
101
+ () =>
102
+ onBlockDrop &&
103
+ ((...args: Parameters<typeof onBlockDrop>) => {
104
+ pinZoomBeforeEdit();
105
+ return onBlockDrop(...args);
106
+ }),
107
+ [onBlockDrop, pinZoomBeforeEdit],
108
+ );
109
+
110
+ return {
111
+ pinZoomBeforeEdit,
112
+ setRangeSelectionRef,
113
+ pinnedOnMoveElement,
114
+ pinnedOnMoveElements,
115
+ pinnedOnResizeElement,
116
+ pinnedOnResizeElements,
117
+ pinnedOnFileDrop,
118
+ pinnedOnAssetDrop,
119
+ pinnedOnBlockDrop,
120
+ };
121
+ }
@@ -0,0 +1,129 @@
1
+ import { useEffect, useMemo, useRef, type RefObject } from "react";
2
+ import { usePlayerStore, type TimelineElement, type ZoomMode } from "../store/playerStore";
3
+ import { getTimelinePixelsPerSecond } from "./timelineZoom";
4
+ import {
5
+ DRAG_EXTEND_MARGIN_PX,
6
+ getTimelineDisplayContentWidth,
7
+ getTimelineFitPps,
8
+ } from "./timelineLayout";
9
+ import type { DraggedClipState, ResizingClipState } from "./useTimelineClipDrag";
10
+
11
+ interface UseTimelineGeometryInput {
12
+ viewportWidth: number;
13
+ effectiveDuration: number;
14
+ zoomMode: ZoomMode;
15
+ manualZoomPercent: number;
16
+ ppsRef: RefObject<number>;
17
+ fitPpsRef: RefObject<number>;
18
+ draggedClip: DraggedClipState | null;
19
+ resizingClip: ResizingClipState | null;
20
+ expandedElements: TimelineElement[];
21
+ isDragging: RefObject<boolean>;
22
+ scrollRef: RefObject<HTMLDivElement | null>;
23
+ lastScrollLeftRef: RefObject<number>;
24
+ }
25
+
26
+ // Derive the timeline's horizontal geometry from the viewport, zoom, and any live
27
+ // drag/resize preview: the pixels-per-second scale, the rendered content width
28
+ // (CapCut-style over-extension while dragging/trimming), and the version key that
29
+ // re-triggers dependent effects after an edit re-derives the elements.
30
+ export function useTimelineGeometry({
31
+ viewportWidth,
32
+ effectiveDuration,
33
+ zoomMode,
34
+ manualZoomPercent,
35
+ ppsRef,
36
+ fitPpsRef,
37
+ draggedClip,
38
+ resizingClip,
39
+ expandedElements,
40
+ isDragging,
41
+ scrollRef,
42
+ lastScrollLeftRef,
43
+ }: UseTimelineGeometryInput) {
44
+ // Fit pps maps at least MIN_TIMELINE_EXTENT_S onto the viewport, so short
45
+ // comps show a 60s ruler with usable empty space (see getTimelineFitPps).
46
+ const fitPps = getTimelineFitPps(viewportWidth, effectiveDuration);
47
+ const pps = getTimelinePixelsPerSecond(fitPps, zoomMode, manualZoomPercent);
48
+ ppsRef.current = pps;
49
+ const trackContentWidth = Math.max(0, effectiveDuration * pps);
50
+ // Drag-to-extend: while a clip is dragged, keep the rendered extent a margin
51
+ // past the ghost's end. Holding the pointer in the right edge zone then keeps
52
+ // auto-scroll stepping (scrollWidth grows with the ghost), so the timeline
53
+ // extends at auto-scroll pace — placing a clip farther than the timeline
54
+ // currently shows. Growth is bounded per frame by AUTO_SCROLL_MAX_SPEED (no
55
+ // fling); leaving the edge zone stops it; the extra width collapses when the
56
+ // drag ends (the composition itself only grows on commit, content-driven).
57
+ const dragGhostEndPx = draggedClip?.started
58
+ ? (draggedClip.previewStart + draggedClip.element.duration) * pps + DRAG_EXTEND_MARGIN_PX
59
+ : 0;
60
+ // Trim-to-extend: same mechanic for a right-edge RESIZE — the rendered extent
61
+ // tracks the trim preview's end so the edge auto-scroll zone always has room
62
+ // to keep stepping while the trim grows past the current timeline width.
63
+ const resizeGhostEndPx = resizingClip?.started
64
+ ? (resizingClip.previewStart + resizingClip.previewDuration) * pps + DRAG_EXTEND_MARGIN_PX
65
+ : 0;
66
+ // The timeline canvas always fills at least the viewport width AND the
67
+ // MIN_TIMELINE_EXTENT_S floor: the ruler + empty track lanes keep going into
68
+ // the space instead of leaving dead black — CapCut-style. Only the RENDERED
69
+ // extent grows; clip positions/durations are untouched.
70
+ const displayContentWidth = getTimelineDisplayContentWidth({
71
+ trackContentWidth,
72
+ viewportWidth,
73
+ pps,
74
+ dragGhostEndPx,
75
+ resizeGhostEndPx,
76
+ });
77
+ const displayDuration = pps > 0 ? displayContentWidth / pps : effectiveDuration;
78
+ const clipStateVersion = useMemo(
79
+ () =>
80
+ expandedElements
81
+ .map((el) => `${el.key ?? el.id}:${el.start}:${el.duration}:${el.track}`)
82
+ .join("|"),
83
+ [expandedElements],
84
+ );
85
+ const zoomModeRef = useRef(zoomMode);
86
+ zoomModeRef.current = zoomMode;
87
+ const manualZoomPercentRef = useRef(manualZoomPercent);
88
+ manualZoomPercentRef.current = manualZoomPercent;
89
+ fitPpsRef.current = fitPps;
90
+
91
+ // Restore the horizontal scroll offset after an edit re-derives the elements
92
+ // (clipStateVersion changes) so the reload doesn't jump the view. Only in manual
93
+ // (pinned) mode — fit mode hides the x-scrollbar (scrollLeft is always 0) — and
94
+ // never mid-drag (auto-scroll owns the offset then). rAF waits for the new layout
95
+ // so the clamp reads the post-resync scrollWidth. zoomMode is a legitimate dep:
96
+ // re-running on a mode flip is a no-op thanks to the guard.
97
+ useEffect(() => {
98
+ if (zoomMode !== "manual" || isDragging.current) return;
99
+ const el = scrollRef.current;
100
+ const target = lastScrollLeftRef.current;
101
+ if (!el || target <= 0) return;
102
+ const raf = requestAnimationFrame(() => {
103
+ const max = Math.max(0, el.scrollWidth - el.clientWidth);
104
+ const next = Math.min(target, max);
105
+ if (Math.abs(el.scrollLeft - next) > 0.5) el.scrollLeft = next;
106
+ });
107
+ return () => cancelAnimationFrame(raf);
108
+ // eslint-disable-next-line react-hooks/exhaustive-deps
109
+ }, [clipStateVersion, zoomMode]);
110
+ // Publish the live scale so edit handlers OUTSIDE <Timeline> (the keyboard-delete
111
+ // path) can pin the zoom via pinTimelineZoomToCurrent without threading geometry.
112
+ // In a useEffect (not the render body) so React-18 concurrent replay — Suspense
113
+ // retry, transitions, StrictMode double-invoke — can't double-publish. The write is
114
+ // idempotent (same pps/fitPps → same fields), so this is behavior-preserving; the
115
+ // effect placement is just the strictly-correct shape.
116
+ useEffect(() => {
117
+ usePlayerStore.getState().setTimelineScale(pps, fitPps);
118
+ }, [pps, fitPps]);
119
+
120
+ return {
121
+ pps,
122
+ fitPps,
123
+ displayContentWidth,
124
+ displayDuration,
125
+ clipStateVersion,
126
+ zoomModeRef,
127
+ manualZoomPercentRef,
128
+ };
129
+ }
@@ -1,5 +1,5 @@
1
1
  import { useRef, useCallback, useEffect, useLayoutEffect } from "react";
2
- import { liveTime, usePlayerStore, type ZoomMode } from "../store/playerStore";
2
+ import { liveTime, type ZoomMode } from "../store/playerStore";
3
3
  import { useMountEffect } from "../../hooks/useMountEffect";
4
4
  import { getPinchTimelineZoomPercent } from "./timelineZoom";
5
5
  import {
@@ -117,21 +117,9 @@ export function useTimelinePlayhead({
117
117
  useMountEffect(() => {
118
118
  const unsub = liveTime.subscribe((t) => {
119
119
  if (!playheadRef.current || durationRef.current <= 0) return;
120
- const playheadX = getTimelinePlayheadLeft(t, ppsRef.current);
121
- playheadRef.current.style.left = `${playheadX}px`;
122
- const scroll = scrollRef.current;
123
- if (
124
- scroll &&
125
- !isDragging.current &&
126
- usePlayerStore.getState().isPlaying &&
127
- shouldAutoScrollTimeline(zoomModeRef.current, scroll.scrollWidth, scroll.clientWidth)
128
- ) {
129
- const edgeMargin = scroll.clientWidth * 0.12;
130
- if (playheadX > scroll.scrollLeft + scroll.clientWidth - edgeMargin)
131
- scroll.scrollLeft = playheadX - scroll.clientWidth * 0.15;
132
- else if (playheadX < scroll.scrollLeft + GUTTER)
133
- scroll.scrollLeft = Math.max(0, playheadX - GUTTER);
134
- }
120
+ // Playback deliberately does NOT scroll the viewport to chase the playhead —
121
+ // the user's scroll position is theirs; the playhead may run off-screen.
122
+ playheadRef.current.style.left = `${getTimelinePlayheadLeft(t, ppsRef.current)}px`;
135
123
  });
136
124
  return unsub;
137
125
  });