@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
@@ -1,52 +1,337 @@
1
+ import { resolveTimelineMove, resolveTimelineResize } from "./timelineEditing";
1
2
  import type { TimelineElement } from "../store/playerStore";
2
- import type { TimelineStackingReorderIntent } from "./timelineEditing";
3
- import type { TimelineLayerId } from "./timelineTrackOrder";
3
+ import { TRACK_H, getTimelineRowFromY, INSERT_BOUNDARY_BAND } from "./timelineLayout";
4
+ import { isMusicTrack, isAudioTimelineElement } from "../../utils/timelineInspector";
5
+ import {
6
+ TIMELINE_SNAP_PX,
7
+ snapMoveToTargets,
8
+ snapTimelineTime,
9
+ type TimelineSnapTarget,
10
+ } from "./timelineSnapping";
11
+ import { resolveInsertRow, resolveZoneDropPlacement } from "./timelineCollision";
12
+ import {
13
+ applyTimelineGroupResizePreview,
14
+ type TimelineGroupResizeSession,
15
+ } from "./timelineGroupEditing";
16
+ import { clampGroupMoveDelta } from "./timelineMultiDragPreview";
17
+ import type { DraggedClipState, ResizingClipState } from "./timelineClipDragTypes";
4
18
 
5
- interface DragPreviewState {
6
- element: Pick<TimelineElement, "track">;
7
- previewLayerId: TimelineLayerId;
8
- previewLayerIndex: number;
19
+ /** Snap-target builder closure supplied by the hook (closes over refs + store). */
20
+ type BuildSnapTargets = (
21
+ excludeElementKey: string | null,
22
+ includeBeats: boolean,
23
+ ) => TimelineSnapTarget[];
24
+
25
+ export interface DragPreviewContext {
26
+ scroll: HTMLDivElement | null;
27
+ pps: number;
28
+ duration: number;
29
+ trackOrder: number[];
30
+ elements: TimelineElement[];
31
+ selectedKeys: ReadonlySet<string>;
32
+ buildSnapTargets: BuildSnapTargets;
33
+ /**
34
+ * The set of tracks that hold audio clips (drives zone-aware drop placement).
35
+ * Frozen for the whole gesture, so the hook builds it ONCE at drag start and
36
+ * passes it in — see useTimelineClipDrag. Absent (e.g. in unit tests) ⇒ built
37
+ * on demand from `elements`, so the result is identical either way.
38
+ */
39
+ audioTracks?: ReadonlySet<number>;
9
40
  }
10
41
 
11
- export interface TimelineMovePreview {
12
- start: number;
13
- track: number;
14
- previewLayerId?: TimelineLayerId;
15
- previewLayerIndex?: number;
16
- stackingReorder?: TimelineStackingReorderIntent | null;
42
+ /**
43
+ * Max start a drag may reach. Allow dragging past the current content into the
44
+ * rendered timeline extent (the viewport-fill keeps that ≥ the viewport width).
45
+ * The composition grows to fit on commit (content-driven duration), so don't
46
+ * cap at content length.
47
+ */
48
+ function resolveDragMaxStart(scroll: HTMLDivElement | null, pps: number, duration: number): number {
49
+ return Math.max(duration, scroll && pps > 0 ? scroll.scrollWidth / pps : duration);
17
50
  }
18
51
 
19
- export interface TimelineGroupMovePreview {
20
- active: boolean;
21
- previewStart: number;
52
+ /**
53
+ * Rigid group move: when the grabbed clip is part of a multi-selection, the
54
+ * WHOLE formation shifts by its delta on commit (see timelineClipDragCommit).
55
+ * Clamp that delta here — against every selected member's start — so the
56
+ * grabbed clip can't out-run the group: it STOPS the instant any member would
57
+ * cross 0, exactly as it lands on commit. Lane changes still apply to the
58
+ * grabbed clip only, so only the start (x) is constrained.
59
+ */
60
+ function resolveGroupClampedStart(
61
+ snapStart: number,
62
+ element: TimelineElement,
63
+ dragKey: string,
64
+ elements: TimelineElement[],
65
+ selectedKeys: ReadonlySet<string>,
66
+ ): number {
67
+ if (selectedKeys.size <= 1 || !selectedKeys.has(dragKey)) return snapStart;
68
+ const memberStarts = elements.filter((e) => selectedKeys.has(e.key ?? e.id)).map((e) => e.start);
69
+ const clampedDelta = clampGroupMoveDelta(snapStart - element.start, memberStarts);
70
+ return element.start + clampedDelta;
22
71
  }
23
72
 
24
- export function resolveDragPreviewPlacement(
25
- drag: DragPreviewState,
26
- nextMove: TimelineMovePreview,
27
- groupMove: TimelineGroupMovePreview,
28
- ): {
73
+ /**
74
+ * The whole drop decision (no same-track overlap, zone-respecting, relocate or
75
+ * create) — one tested pure function, so what runs here is what's verified.
76
+ */
77
+ function resolveDropPlacement(
78
+ drag: DraggedClipState,
79
+ clientY: number,
80
+ previewStart: number,
81
+ desiredTrack: number,
82
+ ctx: DragPreviewContext,
83
+ ): { track: number; insertRow: number | null } {
84
+ const { scroll, trackOrder, elements } = ctx;
85
+ // rowFloat = the pointer's position in track-heights from the top lane; a
86
+ // near-boundary hover requests a deliberate new-track insert. Uses the
87
+ // shared row→y inverse so the top breathing pad is subtracted consistently.
88
+ const rowFloat = scroll
89
+ ? getTimelineRowFromY(clientY - scroll.getBoundingClientRect().top + scroll.scrollTop)
90
+ : 0;
91
+ // Geometry-exact band (the clip inset) so an insert only arms in the visible
92
+ // gutter BETWEEN clip bodies — dragging over a clip body is a lane move, never a
93
+ // phantom insert (the plain-horizontal-drag misfire). See INSERT_BOUNDARY_BAND.
94
+ const rawInsertRow = resolveInsertRow(rowFloat, trackOrder.length, INSERT_BOUNDARY_BAND);
95
+ // Pointer sub-row half: when a drop must auto-create a track (aimed span
96
+ // occupied, no free lane), open it on the side the pointer is nearer.
97
+ const preferInsertAbove = rowFloat - Math.floor(rowFloat) < 0.5;
98
+ const audioTracks =
99
+ ctx.audioTracks ?? new Set(elements.filter(isAudioTimelineElement).map((e) => e.track));
100
+ return resolveZoneDropPlacement({
101
+ order: trackOrder,
102
+ audioTracks,
103
+ elements,
104
+ desiredTrack,
105
+ deliberateInsertRow: rawInsertRow,
106
+ start: previewStart,
107
+ duration: drag.element.duration,
108
+ dragKey: drag.element.key ?? drag.element.id,
109
+ isAudio: isAudioTimelineElement(drag.element),
110
+ preferInsertAbove,
111
+ });
112
+ }
113
+
114
+ /** Recompute the dragged-clip preview (move + snap + group clamp + drop placement). */
115
+ export function computeDragPreview(
116
+ drag: DraggedClipState,
117
+ clientX: number,
118
+ clientY: number,
119
+ ctx: DragPreviewContext,
120
+ ): DraggedClipState {
121
+ const { scroll, pps, duration, trackOrder, elements, selectedKeys, buildSnapTargets } = ctx;
122
+ const dragMaxStart = resolveDragMaxStart(scroll, pps, duration);
123
+ const nextMove = resolveTimelineMove(
124
+ {
125
+ start: drag.element.start,
126
+ track: drag.element.track,
127
+ duration: drag.element.duration,
128
+ originClientX: drag.originClientX,
129
+ originClientY: drag.originClientY,
130
+ originScrollLeft: drag.originScrollLeft,
131
+ originScrollTop: drag.originScrollTop,
132
+ currentScrollLeft: scroll?.scrollLeft ?? drag.originScrollLeft,
133
+ currentScrollTop: scroll?.scrollTop ?? drag.originScrollTop,
134
+ pixelsPerSecond: pps,
135
+ trackHeight: TRACK_H,
136
+ maxStart: dragMaxStart,
137
+ trackOrder,
138
+ },
139
+ clientX,
140
+ clientY,
141
+ );
142
+ // The music track defines the beats, so it must not snap to them —
143
+ // but it still snaps to the playhead and other clip edges.
144
+ const targets = buildSnapTargets(
145
+ drag.element.key ?? drag.element.id,
146
+ !isMusicTrack(drag.element),
147
+ );
148
+ const snap = snapMoveToTargets(
149
+ nextMove.start,
150
+ drag.element.duration,
151
+ targets,
152
+ pps,
153
+ // Relaxed clamp: allow the snapped start past the content, up to the
154
+ // rendered extent (see dragMaxStart) — the composition grows on commit.
155
+ dragMaxStart + drag.element.duration,
156
+ );
157
+ const dragKey = drag.element.key ?? drag.element.id;
158
+ const previewStart = resolveGroupClampedStart(
159
+ snap.start,
160
+ drag.element,
161
+ dragKey,
162
+ elements,
163
+ selectedKeys,
164
+ );
165
+ const { track: previewTrack, insertRow } = resolveDropPlacement(
166
+ drag,
167
+ clientY,
168
+ previewStart,
169
+ nextMove.track,
170
+ ctx,
171
+ );
172
+ return {
173
+ ...drag,
174
+ started: true,
175
+ pointerClientX: clientX,
176
+ pointerClientY: clientY,
177
+ previewStart,
178
+ previewTrack,
179
+ // The lane the POINTER aims at (pre-collision): the commit reads it to tell a
180
+ // deliberate vertical lane change from a horizontal drag merely bumped sideways.
181
+ desiredTrack: nextMove.track,
182
+ insertRow,
183
+ snapTime: snap.snapTime,
184
+ snapType: snap.snapType,
185
+ };
186
+ }
187
+
188
+ export interface ResizePreviewContext {
189
+ scroll: HTMLDivElement | null;
190
+ pps: number;
191
+ buildSnapTargets: BuildSnapTargets;
192
+ }
193
+
194
+ export interface ResizePreviewResult {
195
+ originScrollLeft: number;
29
196
  previewStart: number;
30
- previewTrack: number;
31
- previewLayerId: TimelineLayerId;
32
- previewLayerIndex: number;
33
- previewStackingReorder: TimelineStackingReorderIntent | null;
34
- } {
35
- if (groupMove.active) {
36
- return {
37
- previewStart: groupMove.previewStart,
38
- previewTrack: drag.element.track,
39
- previewLayerId: drag.previewLayerId,
40
- previewLayerIndex: drag.previewLayerIndex,
41
- previewStackingReorder: null,
42
- };
197
+ previewDuration: number;
198
+ previewPlaybackStart?: number;
199
+ }
200
+
201
+ /** Compute the trim preview for a pointer x (pure — the hook applies the state). */
202
+ // fallow-ignore-next-line complexity
203
+ export function computeResizePreview(
204
+ resize: ResizingClipState,
205
+ clientX: number,
206
+ ctx: ResizePreviewContext,
207
+ ): ResizePreviewResult {
208
+ const { scroll, pps, buildSnapTargets } = ctx;
209
+ // Scroll compensation: auto-scroll moves the content while the pointer stays
210
+ // put, so fold the scroll delta into the pointer x (mirrors
211
+ // resolveTimelineMove's originScrollLeft handling).
212
+ const originScrollLeft = resize.originScrollLeft ?? scroll?.scrollLeft ?? 0;
213
+ const effectiveClientX = clientX + ((scroll?.scrollLeft ?? originScrollLeft) - originScrollLeft);
214
+
215
+ const sourceRemaining =
216
+ resize.element.sourceDuration != null
217
+ ? Math.max(
218
+ 0,
219
+ (resize.element.sourceDuration - (resize.element.playbackStart ?? 0)) /
220
+ Math.max(resize.element.playbackRate ?? 1, 0.1),
221
+ )
222
+ : Number.POSITIVE_INFINITY;
223
+ const normalizedTag = resize.element.tag.toLowerCase();
224
+ const canSeedPlaybackStart = normalizedTag === "audio" || normalizedTag === "video";
225
+ const playbackRate = Math.max(resize.element.playbackRate ?? 1, 0.1);
226
+ // Trim limit = available source media only — NOT the composition length.
227
+ // Duration is content-driven (the comp grows/shrinks to fit on commit), so
228
+ // capping a trim at the current comp end both blocked extending the last clip
229
+ // rightward and, after a far move, collapsed a clip to the sliver between its
230
+ // start and the comp end (the 8s→0.95s audio incident). Images/text/shapes
231
+ // have no source, so they extend freely.
232
+ const maxEnd = resize.element.start + sourceRemaining;
233
+ let nextResize = resolveTimelineResize(
234
+ {
235
+ start: resize.element.start,
236
+ duration: resize.element.duration,
237
+ originClientX: resize.originClientX,
238
+ pixelsPerSecond: pps,
239
+ minStart: 0,
240
+ maxEnd,
241
+ playbackStart:
242
+ resize.edge === "start" && canSeedPlaybackStart
243
+ ? (resize.element.playbackStart ?? 0)
244
+ : resize.element.playbackStart,
245
+ playbackRate: resize.element.playbackRate,
246
+ },
247
+ resize.edge,
248
+ effectiveClientX,
249
+ );
250
+
251
+ // Snap edge to unified targets (beats + clip edges + playhead) when available.
252
+ // The snap must stay inside the same limits resolveTimelineResize enforces, or
253
+ // it would push the edge past the available source media / composition end.
254
+ // The music track defines the beats, so it must not snap to them — but it
255
+ // still snaps to the playhead and other clip edges.
256
+ const trimTargets = buildSnapTargets(
257
+ resize.element.key ?? resize.element.id,
258
+ !isMusicTrack(resize.element),
259
+ );
260
+ if (trimTargets.length > 0) {
261
+ const snapSecs = TIMELINE_SNAP_PX / Math.max(pps, 1);
262
+ if (resize.edge === "end") {
263
+ const edgeTime = nextResize.start + nextResize.duration;
264
+ const snapped = snapTimelineTime(edgeTime, trimTargets, snapSecs).time;
265
+ // Stay within [start+minDuration, maxEnd] so the snap can't create a
266
+ // degenerate clip or run past the source/composition limit.
267
+ const snappedDuration = Math.round((snapped - nextResize.start) * 1000) / 1000;
268
+ if (snapped !== edgeTime && snapped <= maxEnd + 1e-6 && snappedDuration >= 0.05) {
269
+ nextResize = { ...nextResize, duration: snappedDuration };
270
+ }
271
+ } else {
272
+ const snapped = snapTimelineTime(nextResize.start, trimTargets, snapSecs).time;
273
+ const delta = nextResize.start - snapped; // >0 when snapping left
274
+ // Leftward snap reveals more source; cap so playbackStart can't go < 0.
275
+ const maxLeftDelta =
276
+ nextResize.playbackStart != null
277
+ ? nextResize.playbackStart / playbackRate
278
+ : Number.POSITIVE_INFINITY;
279
+ // Also require the resulting duration to stay >= minDuration so a rightward
280
+ // snap (delta < 0) can't collapse the clip to zero/negative.
281
+ const snappedDuration = Math.round((nextResize.duration + delta) * 1000) / 1000;
282
+ if (
283
+ snapped !== nextResize.start &&
284
+ snapped >= 0 &&
285
+ delta <= maxLeftDelta + 1e-6 &&
286
+ snappedDuration >= 0.05
287
+ ) {
288
+ nextResize = {
289
+ ...nextResize,
290
+ start: snapped,
291
+ duration: snappedDuration,
292
+ playbackStart:
293
+ nextResize.playbackStart != null
294
+ ? Math.round(Math.max(0, nextResize.playbackStart - delta * playbackRate) * 1000) /
295
+ 1000
296
+ : undefined,
297
+ };
298
+ }
299
+ }
43
300
  }
44
301
 
45
302
  return {
46
- previewStart: groupMove.previewStart,
47
- previewTrack: nextMove.track,
48
- previewLayerId: nextMove.previewLayerId ?? drag.previewLayerId,
49
- previewLayerIndex: nextMove.previewLayerIndex ?? drag.previewLayerIndex,
50
- previewStackingReorder: nextMove.stackingReorder ?? null,
303
+ originScrollLeft,
304
+ previewStart: nextResize.start,
305
+ previewDuration: nextResize.duration,
306
+ previewPlaybackStart: nextResize.playbackStart,
51
307
  };
52
308
  }
309
+
310
+ /**
311
+ * Apply a rigid group-resize preview: fold the grabbed clip's raw delta into the
312
+ * session, preview every non-grabbed member through the store (`updateElement`),
313
+ * and set the grabbed clip's preview state (it renders from resizingClip state, so
314
+ * its store value stays pristine until commit — like the single-clip path).
315
+ */
316
+ export function previewGroupResize(
317
+ session: TimelineGroupResizeSession,
318
+ next: ResizePreviewResult,
319
+ grabbedKey: string,
320
+ updateElement: (
321
+ key: string,
322
+ patch: { start: number; duration: number; playbackStart?: number },
323
+ ) => void,
324
+ setResizeState: (v: ResizePreviewResult) => void,
325
+ ): void {
326
+ const grabbedChange = applyTimelineGroupResizePreview(session, next);
327
+ for (const c of session.changes) {
328
+ if (c.key === grabbedKey) continue;
329
+ updateElement(c.key, { start: c.start, duration: c.duration, playbackStart: c.playbackStart });
330
+ }
331
+ setResizeState({
332
+ originScrollLeft: next.originScrollLeft,
333
+ previewStart: grabbedChange?.start ?? next.previewStart,
334
+ previewDuration: grabbedChange?.duration ?? next.previewDuration,
335
+ previewPlaybackStart: grabbedChange?.playbackStart ?? next.previewPlaybackStart,
336
+ });
337
+ }
@@ -0,0 +1,65 @@
1
+ import type { TimelineElement } from "../store/playerStore";
2
+ import type { TimelineSnapType } from "./timelineSnapping";
3
+ import type { BlockedTimelineEditIntent } from "./timelineEditing";
4
+
5
+ /* ── Shared clip-drag state types ───────────────────────────────── */
6
+ export interface DraggedClipState {
7
+ element: TimelineElement;
8
+ originClientX: number;
9
+ originClientY: number;
10
+ originScrollLeft: number;
11
+ originScrollTop: number;
12
+ pointerClientX: number;
13
+ pointerClientY: number;
14
+ pointerOffsetX: number;
15
+ pointerOffsetY: number;
16
+ previewStart: number;
17
+ previewTrack: number;
18
+ /**
19
+ * The lane the POINTER aims at (from the drag's vertical position), before
20
+ * collision resolution. `previewTrack` is where the clip actually LANDS —
21
+ * which may differ from `desiredTrack` when the aimed span is occupied and the
22
+ * collision rules bump the dragged clip to a free lane. The commit reads this
23
+ * to tell a deliberate VERTICAL lane change (pointer aimed at another lane →
24
+ * stacking sync allowed) from a plain HORIZONTAL drag whose clip merely got
25
+ * bumped sideways (never touches z). Optional: when absent the commit falls
26
+ * back to `previewTrack` (pre-existing behaviour).
27
+ */
28
+ desiredTrack?: number;
29
+ /**
30
+ * When non-null, the drop inserts a NEW track at this visual row boundary
31
+ * (0 = above the top lane, trackOrder.length = below the bottom) instead of
32
+ * landing on previewTrack. Drives the insertion-line indicator.
33
+ */
34
+ insertRow: number | null;
35
+ /** Snap target the clip will land on, for the guide highlight. */
36
+ snapTime: number | null;
37
+ snapType: TimelineSnapType | null;
38
+ started: boolean;
39
+ }
40
+
41
+ export interface ResizingClipState {
42
+ element: TimelineElement;
43
+ edge: "start" | "end";
44
+ originClientX: number;
45
+ /**
46
+ * scrollLeft at gesture start. Edge auto-scroll moves the content under a
47
+ * stationary pointer, so the trim math folds (current − origin) scrollLeft
48
+ * into the pointer x — mirroring resolveTimelineMove's scroll compensation.
49
+ * Optional so pre-existing constructors/tests stay valid; when absent the
50
+ * first preview update captures the current scrollLeft.
51
+ */
52
+ originScrollLeft?: number;
53
+ previewStart: number;
54
+ previewDuration: number;
55
+ previewPlaybackStart?: number;
56
+ started: boolean;
57
+ }
58
+
59
+ export interface BlockedClipState {
60
+ element: TimelineElement;
61
+ intent: BlockedTimelineEditIntent;
62
+ originClientX: number;
63
+ originClientY: number;
64
+ started: boolean;
65
+ }