@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
@@ -2,6 +2,10 @@ import { useState, useRef, useEffect, useCallback } from "react";
2
2
  import { ContextMenu } from "./AssetContextMenu";
3
3
  import { basename, getAudioSubtype } from "./assetHelpers";
4
4
  import { TIMELINE_ASSET_MIME } from "../../utils/timelineAssetDrop";
5
+ import { usePlayerStore } from "../../player/store/playerStore";
6
+ import { useAssetPreviewStore } from "../../utils/assetPreviewStore";
7
+ import { findClipForAsset, isPointerClick } from "../../utils/assetClickBehavior";
8
+ import { resolveMediaPreviewUrl } from "../../player/components/thumbnailUtils";
5
9
 
6
10
  export function AudioRow({
7
11
  projectId,
@@ -12,6 +16,7 @@ export function AudioRow({
12
16
  isCopied,
13
17
  onDelete,
14
18
  onRename,
19
+ onAddAssetToTimeline,
15
20
  }: {
16
21
  projectId: string;
17
22
  asset: string;
@@ -21,6 +26,7 @@ export function AudioRow({
21
26
  isCopied: boolean;
22
27
  onDelete?: (path: string) => void;
23
28
  onRename?: (oldPath: string, newPath: string) => void;
29
+ onAddAssetToTimeline?: (path: string) => void;
24
30
  }) {
25
31
  const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null);
26
32
  const [playing, setPlaying] = useState(false);
@@ -32,7 +38,53 @@ export function AudioRow({
32
38
  const animRef = useRef<number>(0);
33
39
  const name = basename(asset);
34
40
  const subtype = getAudioSubtype(asset);
35
- const serveUrl = `/api/projects/${projectId}/preview/${asset}`;
41
+ const serveUrl = resolveMediaPreviewUrl(asset, projectId);
42
+
43
+ // CapCut-style click behavior: drag-threshold gate.
44
+ const pointerDownRef = useRef<{ x: number; y: number } | null>(null);
45
+ const setSelectedElementId = usePlayerStore((s) => s.setSelectedElementId);
46
+ const requestClipReveal = usePlayerStore((s) => s.requestClipReveal);
47
+ const elements = usePlayerStore((s) => s.elements);
48
+ const setPreviewAsset = useAssetPreviewStore((s) => s.setPreviewAsset);
49
+ const clearPreviewAsset = useAssetPreviewStore((s) => s.clearPreviewAsset);
50
+
51
+ const handlePointerDown = useCallback((e: React.PointerEvent) => {
52
+ pointerDownRef.current = { x: e.clientX, y: e.clientY };
53
+ }, []);
54
+
55
+ const handlePointerUp = useCallback(
56
+ (e: React.PointerEvent) => {
57
+ const origin = pointerDownRef.current;
58
+ pointerDownRef.current = null;
59
+ if (!origin) return;
60
+ if (!isPointerClick(e.clientX - origin.x, e.clientY - origin.y)) return;
61
+ if (used) {
62
+ const clip = findClipForAsset(elements, asset);
63
+ if (clip) {
64
+ // Dismiss any open preview overlay (from another asset) — the reveal
65
+ // must not leave a stale preview card floating over the canvas.
66
+ clearPreviewAsset();
67
+ const clipKey = clip.key ?? clip.id;
68
+ setSelectedElementId(clipKey);
69
+ // Scroll the timeline so the selected clip is actually visible.
70
+ requestClipReveal(clipKey);
71
+ return;
72
+ }
73
+ }
74
+ // Not added → preview overlay (audio player)
75
+ setPreviewAsset(asset, projectId);
76
+ },
77
+ [
78
+ used,
79
+ elements,
80
+ asset,
81
+ projectId,
82
+ setSelectedElementId,
83
+ requestClipReveal,
84
+ setPreviewAsset,
85
+ clearPreviewAsset,
86
+ ],
87
+ );
36
88
 
37
89
  useEffect(() => {
38
90
  return () => {
@@ -109,7 +161,8 @@ export function AudioRow({
109
161
  <>
110
162
  <div
111
163
  draggable
112
- onClick={() => onCopy(asset)}
164
+ onPointerDown={handlePointerDown}
165
+ onPointerUp={handlePointerUp}
113
166
  onDragStart={(e) => {
114
167
  e.dataTransfer.effectAllowed = "copy";
115
168
  e.dataTransfer.setData(TIMELINE_ASSET_MIME, JSON.stringify({ path: asset }));
@@ -195,6 +248,7 @@ export function AudioRow({
195
248
  onCopy={onCopy}
196
249
  onDelete={onDelete}
197
250
  onRename={onRename}
251
+ onAddAtPlayhead={onAddAssetToTimeline}
198
252
  />
199
253
  )}
200
254
  </>
@@ -10,6 +10,7 @@ import {
10
10
  import { usePlayerStore } from "../../player";
11
11
  import { formatTime } from "../../player/lib/time";
12
12
  import { useStudioShellContext } from "../../contexts/StudioContext";
13
+ import { TIMELINE_BLOCK_MIME } from "../../utils/timelineAssetDrop";
13
14
  export interface BlockPreviewInfo {
14
15
  videoUrl?: string;
15
16
  posterUrl?: string;
@@ -383,10 +384,16 @@ function BlockCard({
383
384
  return (
384
385
  <div
385
386
  className="group/card rounded-md overflow-hidden cursor-pointer transition-colors bg-neutral-900 hover:bg-neutral-800"
387
+ draggable
388
+ onDragStart={(e) => {
389
+ e.dataTransfer.effectAllowed = "copy";
390
+ e.dataTransfer.setData(TIMELINE_BLOCK_MIME, JSON.stringify({ name }));
391
+ e.dataTransfer.setData("text/plain", name);
392
+ handleLeave(); // cancel the hover-preview timer so it doesn't fire mid-drag
393
+ }}
386
394
  onPointerEnter={handleEnter}
387
395
  onPointerLeave={handleLeave}
388
396
  >
389
- {/* Thumbnail */}
390
397
  <div className="aspect-video w-full overflow-hidden relative">
391
398
  {hovered && videoUrl ? (
392
399
  <video
@@ -60,6 +60,7 @@ interface LeftSidebarProps {
60
60
  onAddBlock?: (blockName: string) => void;
61
61
  onPreviewBlock?: (preview: BlockPreviewInfo | null) => void;
62
62
  takeoverContent?: ReactNode;
63
+ onAddAssetToTimeline?: (path: string) => void;
63
64
  }
64
65
 
65
66
  export const LeftSidebar = memo(
@@ -92,6 +93,7 @@ export const LeftSidebar = memo(
92
93
  onAddBlock,
93
94
  onPreviewBlock,
94
95
  takeoverContent,
96
+ onAddAssetToTimeline,
95
97
  },
96
98
  ref,
97
99
  ) {
@@ -111,7 +113,7 @@ export const LeftSidebar = memo(
111
113
 
112
114
  return (
113
115
  <div
114
- className="flex flex-col h-full bg-neutral-950 border-r border-neutral-800/50"
116
+ className="flex flex-col h-full overflow-hidden rounded-lg border border-neutral-800/50 bg-neutral-950"
115
117
  style={{ width }}
116
118
  >
117
119
  {takeoverContent ? (
@@ -230,6 +232,7 @@ export const LeftSidebar = memo(
230
232
  onImport={onImportFiles}
231
233
  onDelete={onDeleteFile}
232
234
  onRename={onRenameFile}
235
+ onAddAssetToTimeline={onAddAssetToTimeline}
233
236
  />
234
237
  )}
235
238
  {tab === "code" && (
@@ -30,6 +30,35 @@ export function ext(path: string): string {
30
30
  return dot > 0 ? name.slice(dot + 1).toUpperCase() : "";
31
31
  }
32
32
 
33
+ /**
34
+ * Truncate a string to at most `maxLen` chars, preserving the start and end.
35
+ * Middle characters are replaced with an ellipsis. If the string is short
36
+ * enough it is returned unchanged.
37
+ *
38
+ * @example truncateMiddle("2a37eabf-long-uuid-887d8.mp4", 20) → "2a37eabf-…887d8.mp4"
39
+ *
40
+ * Pure — unit-tested.
41
+ */
42
+ export function truncateMiddle(str: string, maxLen: number): string {
43
+ if (str.length <= maxLen) return str;
44
+ const keep = maxLen - 1; // 1 char for ellipsis
45
+ const tail = Math.floor(keep / 3);
46
+ const head = keep - tail;
47
+ return str.slice(0, head) + "…" + str.slice(str.length - tail);
48
+ }
49
+
50
+ /**
51
+ * Format a duration in seconds as MM:SS. Returns an empty string for
52
+ * non-positive, NaN, or Infinity values. Pure — unit-tested.
53
+ */
54
+ export function formatDuration(seconds: number): string {
55
+ if (!Number.isFinite(seconds) || seconds <= 0) return "";
56
+ const total = Math.round(seconds);
57
+ const m = Math.floor(total / 60);
58
+ const s = total % 60;
59
+ return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
60
+ }
61
+
33
62
  export const CATEGORY_LABELS: Record<MediaCategory, string> = {
34
63
  audio: "Audio",
35
64
  images: "Images",
@@ -98,6 +98,15 @@ export function useDomEditActionsContext(): DomEditActionsValue {
98
98
  return ctx;
99
99
  }
100
100
 
101
+ /**
102
+ * Optional access — returns null outside a provider. Lets the player-package
103
+ * <Timeline> (a public standalone export) reach the z-order persist path when
104
+ * embedded in the NLE without hard-requiring the provider in standalone/test mounts.
105
+ */
106
+ export function useDomEditActionsContextOptional(): DomEditActionsValue | null {
107
+ return useContext(DomEditActionsContext);
108
+ }
109
+
101
110
  export function useDomEditSelectionContext(): DomEditSelectionValue {
102
111
  const ctx = useContext(DomEditSelectionContext);
103
112
  if (!ctx) throw new Error("useDomEditSelectionContext must be used within DomEditProvider");
@@ -31,8 +31,6 @@ export interface StudioShellValue {
31
31
  compositionDimensions: CompositionDimensions | null;
32
32
  waitForPendingDomEditSaves: () => Promise<void>;
33
33
  handlePreviewIframeRef: (iframe: HTMLIFrameElement | null) => void;
34
- timelineVisible: boolean;
35
- toggleTimelineVisibility: () => void;
36
34
  }
37
35
 
38
36
  export interface StudioPlaybackValue {
@@ -56,6 +54,15 @@ export function useStudioShellContext(): StudioShellValue {
56
54
  return ctx;
57
55
  }
58
56
 
57
+ /**
58
+ * Optional access — returns null outside a provider. Lets the player-package
59
+ * <Timeline> (a public standalone export) read shell state when embedded in the
60
+ * NLE without hard-requiring the provider in standalone/test mounts.
61
+ */
62
+ export function useStudioShellContextOptional(): StudioShellValue | null {
63
+ return useContext(StudioShellContext);
64
+ }
65
+
59
66
  export function useStudioPlaybackContext(): StudioPlaybackValue {
60
67
  const ctx = useContext(StudioPlaybackContext);
61
68
  if (!ctx) throw new Error("useStudioPlaybackContext must be used within StudioPlaybackProvider");
@@ -90,8 +97,6 @@ export function StudioShellProvider({
90
97
  compositionDimensions,
91
98
  waitForPendingDomEditSaves,
92
99
  handlePreviewIframeRef,
93
- timelineVisible,
94
- toggleTimelineVisibility,
95
100
  } = value;
96
101
 
97
102
  const stable = useMemo<StudioShellValue>(
@@ -108,14 +113,11 @@ export function StudioShellProvider({
108
113
  compositionDimensions,
109
114
  waitForPendingDomEditSaves,
110
115
  handlePreviewIframeRef,
111
- timelineVisible,
112
- toggleTimelineVisibility,
113
116
  }),
114
117
  [
115
118
  projectId,
116
119
  activeCompPath,
117
120
  compositionDimensions,
118
- timelineVisible,
119
121
  editHistory,
120
122
  renderQueue,
121
123
  setActiveCompPath,
@@ -125,7 +127,6 @@ export function StudioShellProvider({
125
127
  handleRedo,
126
128
  waitForPendingDomEditSaves,
127
129
  handlePreviewIframeRef,
128
- toggleTimelineVisibility,
129
130
  ],
130
131
  );
131
132
  return <StudioShellContext value={stable}>{children}</StudioShellContext>;
@@ -10,7 +10,7 @@ export function useTimelineEditContext(): TimelineEditCallbacks {
10
10
  }
11
11
 
12
12
  /**
13
- * Optional access, returns an empty object when outside a provider.
13
+ * Optional access returns an empty object when outside a provider.
14
14
  * Useful in components that can render both inside and outside the NLE.
15
15
  */
16
16
  export function useTimelineEditContextOptional(): TimelineEditCallbacks {
@@ -26,18 +26,14 @@ export function TimelineEditProvider({
26
26
  }) {
27
27
  const memoized = useMemo(
28
28
  () => value,
29
- // Each callback is a stable reference from the parent, memoize the bag
29
+ // Each callback is a stable reference from the parent memoize the bag
30
30
  // so consumers don't re-render when unrelated parent state changes.
31
31
  // eslint-disable-next-line react-hooks/exhaustive-deps
32
32
  [
33
33
  value.onMoveElement,
34
- value.onResizeElement,
35
34
  value.onMoveElements,
36
- value.onResizeElements,
37
- value.onPreviewMoveElements,
38
- value.onPreviewResizeElements,
35
+ value.onResizeElement,
39
36
  value.onToggleTrackHidden,
40
- value.onToggleElementHidden,
41
37
  value.onBlockedEditAttempt,
42
38
  value.onSplitElement,
43
39
  value.onRazorSplit,
@@ -0,0 +1,35 @@
1
+ import { usePlayerStore } from "../player/store/playerStore";
2
+ import { selectedKeyframePercentagesForElement } from "../utils/keyframeSelection";
3
+ import type { CommitMutationOptions } from "./gsapScriptCommitTypes";
4
+
5
+ let deleteKeyframesCommitCounter = 0;
6
+
7
+ /**
8
+ * Remove the keyframes currently selected in the player store from the active
9
+ * element's GSAP animation. Reads selection lazily so it stays correct when
10
+ * invoked from a ref callback.
11
+ */
12
+ export function deleteSelectedKeyframes(session: {
13
+ selectedGsapAnimations: readonly { id: string; keyframes?: unknown }[];
14
+ handleGsapRemoveKeyframe: (
15
+ animId: string,
16
+ pct: number,
17
+ options?: Partial<CommitMutationOptions>,
18
+ ) => void;
19
+ }): void {
20
+ const { selectedKeyframes, selectedElementId } = usePlayerStore.getState();
21
+ const animation = session.selectedGsapAnimations.find((anim) => anim.keyframes);
22
+ if (!animation) return;
23
+ // Only the active element's keyframes; a stale cross-element selection must not delete here.
24
+ const percentages = selectedKeyframePercentagesForElement(selectedKeyframes, selectedElementId);
25
+ const coalesceOptions = {
26
+ coalesceKey: `delete-keyframes:${++deleteKeyframesCommitCounter}`,
27
+ coalesceMs: Number.POSITIVE_INFINITY,
28
+ };
29
+ for (const [index, pct] of percentages.entries()) {
30
+ session.handleGsapRemoveKeyframe(animation.id, pct, {
31
+ ...coalesceOptions,
32
+ ...(index === percentages.length - 1 ? { softReload: true } : { skipReload: true }),
33
+ });
34
+ }
35
+ }
@@ -1,5 +1,26 @@
1
1
  import type { DomEditSelection } from "../components/editor/domEditing";
2
- import type { PatchOperation } from "../utils/sourcePatcher";
2
+ import type { PatchOperation, PatchTarget } from "../utils/sourcePatcher";
3
+
4
+ export interface DomEditPatchBatch {
5
+ sourceFile: string;
6
+ patches: Array<{ target: PatchTarget; operations: PatchOperation[] }>;
7
+ }
8
+
9
+ export type CommitDomEditPatchBatches = (
10
+ batches: DomEditPatchBatch[],
11
+ options: {
12
+ label: string;
13
+ coalesceKey: string;
14
+ /**
15
+ * Request skipping the preview iframe reload after a successful persist.
16
+ * Only honored when the persist is provably in sync with the live DOM:
17
+ * every patch operation is inline-style-only AND the server matched every
18
+ * patch target. Any unmatched target (or a non-style op) falls back to the
19
+ * reload so the preview reconverges with disk. Default: always reload.
20
+ */
21
+ skipReload?: boolean;
22
+ },
23
+ ) => Promise<void>;
3
24
 
4
25
  export type PersistDomEditOperations = (
5
26
  selection: DomEditSelection,
@@ -7,6 +28,7 @@ export type PersistDomEditOperations = (
7
28
  options?: {
8
29
  label?: string;
9
30
  coalesceKey?: string;
31
+ coalesceMs?: number;
10
32
  skipRefresh?: boolean;
11
33
  prepareContent?: (html: string, sourceFile: string) => string;
12
34
  shouldSave?: () => boolean;
@@ -1,5 +1,8 @@
1
1
  // Shared harness helpers for selection hook tests (useDomSelection,
2
2
  // usePreviewInteraction). Test-only module.
3
+ import type React from "react";
4
+ import { act } from "react";
5
+ import { createRoot, type Root } from "react-dom/client";
3
6
  import type { DomEditSelection } from "../components/editor/domEditing";
4
7
 
5
8
  export function installReactActEnvironment(): void {
@@ -9,6 +12,17 @@ export function installReactActEnvironment(): void {
9
12
  });
10
13
  }
11
14
 
15
+ /** Mount a React element into a fresh detached host and return its root. */
16
+ export function mountReactHarness(node: React.ReactElement): Root {
17
+ const host = document.createElement("div");
18
+ document.body.append(host);
19
+ const root = createRoot(host);
20
+ act(() => {
21
+ root.render(node);
22
+ });
23
+ return root;
24
+ }
25
+
12
26
  export function makeSelection(label: string, element: HTMLElement): DomEditSelection {
13
27
  return {
14
28
  element,