@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio",
3
- "version": "0.7.54",
3
+ "version": "0.7.56",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -46,11 +46,11 @@
46
46
  "gsap": "^3.13.0",
47
47
  "marked": "^14.1.4",
48
48
  "mediabunny": "^1.45.3",
49
- "@hyperframes/player": "0.7.54",
50
- "@hyperframes/sdk": "0.7.54",
51
- "@hyperframes/parsers": "0.7.54",
52
- "@hyperframes/studio-server": "0.7.54",
53
- "@hyperframes/core": "0.7.54"
49
+ "@hyperframes/player": "0.7.56",
50
+ "@hyperframes/studio-server": "0.7.56",
51
+ "@hyperframes/sdk": "0.7.56",
52
+ "@hyperframes/core": "0.7.56",
53
+ "@hyperframes/parsers": "0.7.56"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@types/react": "19",
@@ -65,7 +65,7 @@
65
65
  "vite": "^6.4.2",
66
66
  "vitest": "^3.2.4",
67
67
  "zustand": "^5.0.0",
68
- "@hyperframes/producer": "0.7.54"
68
+ "@hyperframes/producer": "0.7.56"
69
69
  },
70
70
  "peerDependencies": {
71
71
  "react": "19",
package/src/App.tsx CHANGED
@@ -1,7 +1,7 @@
1
1
  import { useState, useCallback, useRef, useMemo, useEffect, useLayoutEffect } from "react";
2
2
  import type { LeftSidebarHandle, SidebarTab } from "./components/sidebar/LeftSidebar";
3
3
  import { useRenderQueue } from "./components/renders/useRenderQueue";
4
- import { usePlayerStore } from "./player";
4
+ import { usePlayerStore, type TimelineElement } from "./player";
5
5
  import { StudioOverlays } from "./components/StudioOverlays";
6
6
  import { SaveQueuePausedBanner } from "./components/SaveQueuePausedBanner";
7
7
  import { useCaptionStore } from "./captions/store";
@@ -10,17 +10,21 @@ import { usePersistentEditHistory } from "./hooks/usePersistentEditHistory";
10
10
  import { usePanelLayout } from "./hooks/usePanelLayout";
11
11
  import { useFileManager } from "./hooks/useFileManager";
12
12
  import { usePreviewPersistence } from "./hooks/usePreviewPersistence";
13
+ import { usePreviewDocumentVersion } from "./hooks/usePreviewDocumentVersion";
13
14
  import { useTimelineEditing } from "./hooks/useTimelineEditing";
15
+ import {
16
+ persistTimelineMoveEditsAtomically,
17
+ type TimelineMoveOperation,
18
+ } from "./hooks/timelineMoveAdapter";
14
19
  import type { TimelineZIndexReorderCommit } from "./hooks/useTimelineEditingTypes";
20
+ import type { TimelineStackingReorderIntent } from "./player/components/timelineStacking";
15
21
  import type { BlockPreviewInfo } from "./components/sidebar/BlocksTab";
16
22
  import { useDomEditSession } from "./hooks/useDomEditSession";
17
23
  import { useSdkSelectionSync } from "./hooks/useSdkSelectionSync";
18
24
  import { useStudioSdkSessions } from "./hooks/useStudioSdkSessions";
19
- import { usePreviewDocumentVersion } from "./hooks/usePreviewDocumentVersion";
20
25
  import { useBlockHandlers } from "./hooks/useBlockHandlers";
21
26
  import { useAppHotkeys } from "./hooks/useAppHotkeys";
22
27
  import { useClipboard } from "./hooks/useClipboard";
23
- import { readStudioUiPreferences, writeStudioUiPreferences } from "./utils/studioUiPreferences";
24
28
  import { deleteSelectedKeyframes } from "./hooks/timelineEditingHelpers";
25
29
  import { useCaptionDetection } from "./hooks/useCaptionDetection";
26
30
  import { useRenderClipContent } from "./hooks/useRenderClipContent";
@@ -33,7 +37,7 @@ import { useCompositionContentLoader } from "./hooks/useCompositionContentLoader
33
37
  import { useStudioUrlState } from "./hooks/useStudioUrlState";
34
38
  import {
35
39
  buildStudioContextValue,
36
- useDragOverlay,
40
+ useGlobalFileDrop,
37
41
  useInspectorState,
38
42
  } from "./hooks/useStudioContextValue";
39
43
  import type { DomEditSelection } from "./components/editor/domEditing";
@@ -42,7 +46,7 @@ import { useGestureCommit } from "./hooks/useGestureCommit";
42
46
  import { STUDIO_KEYFRAMES_ENABLED } from "./components/editor/manualEditingAvailability";
43
47
  import { GestureTrailOverlay } from "./components/editor/GestureTrailOverlay";
44
48
  import { StudioLeftSidebar } from "./components/StudioLeftSidebar";
45
- import { StudioPreviewArea } from "./components/StudioPreviewArea";
49
+ import { EditorShell } from "./components/EditorShell";
46
50
  import { StudioRightPanel } from "./components/StudioRightPanel";
47
51
  import { TimelineToolbar } from "./components/TimelineToolbar";
48
52
  import { StudioPlaybackProvider, StudioShellProvider } from "./contexts/StudioContext";
@@ -108,18 +112,6 @@ export function StudioApp() {
108
112
  : 0;
109
113
  return Math.max(timelineDuration, maxEnd);
110
114
  }, [timelineDuration, timelineElements]);
111
- const [timelineVisible, setTimelineVisible] = useState(
112
- () =>
113
- initialUrlStateRef.current.timelineVisible ??
114
- readStudioUiPreferences().timelineVisible ??
115
- true,
116
- );
117
- const toggleTimelineVisibility = useCallback(() => {
118
- setTimelineVisible((v) => {
119
- writeStudioUiPreferences({ timelineVisible: !v });
120
- return !v;
121
- });
122
- }, []);
123
115
  const { toasts, showToast, dismissToast } = useToast();
124
116
  const panelLayout = usePanelLayout({
125
117
  rightCollapsed: initialUrlStateRef.current.rightCollapsed,
@@ -187,6 +179,31 @@ export function StudioApp() {
187
179
  forceReloadSdkSession: sdkHandle.forceReload,
188
180
  handleDomZIndexReorderCommitRef,
189
181
  });
182
+ const handleTimelineElementsMove = useCallback(
183
+ async (
184
+ edits: Array<{
185
+ element: TimelineElement;
186
+ updates: Pick<TimelineElement, "start" | "track"> & {
187
+ stackingReorder?: TimelineStackingReorderIntent | null;
188
+ };
189
+ }>,
190
+ coalesceKey?: string,
191
+ operation: TimelineMoveOperation = "timing",
192
+ ) => {
193
+ await persistTimelineMoveEditsAtomically(edits, coalesceKey, operation, {
194
+ handleTimelineGroupMove: timelineEditing.handleTimelineGroupMove,
195
+ });
196
+ },
197
+ [timelineEditing.handleTimelineGroupMove],
198
+ );
199
+ const handleAddAssetAtPlayhead = useCallback(
200
+ (assetPath: string) =>
201
+ timelineEditing.handleTimelineAssetDrop(assetPath, {
202
+ start: usePlayerStore.getState().currentTime,
203
+ track: 0,
204
+ }),
205
+ [timelineEditing],
206
+ );
190
207
  const {
191
208
  activeBlockParams,
192
209
  setActiveBlockParams,
@@ -232,7 +249,6 @@ export function StudioApp() {
232
249
  previewIframeRef,
233
250
  });
234
251
  const appHotkeys = useAppHotkeys({
235
- toggleTimelineVisibility,
236
252
  handleTimelineElementDelete: timelineEditing.handleTimelineElementDelete,
237
253
  handleTimelineElementSplit: timelineEditing.handleTimelineElementSplit,
238
254
  handleDomEditElementDelete: domEditDeleteBridge,
@@ -348,7 +364,7 @@ export function StudioApp() {
348
364
  setConsoleErrors,
349
365
  resetErrors: resetConsoleErrors,
350
366
  } = useConsoleErrorCapture(previewIframe);
351
- const dragOverlay = useDragOverlay(fileManager.handleImportFiles);
367
+ const dragOverlay = useGlobalFileDrop(timelineEditing.handleTimelineFileDrop);
352
368
  const handleToggleRecordingRef = useRef<() => void>(() => {});
353
369
  const domEditSessionRef = useRef(domEditSession);
354
370
  domEditSessionRef.current = domEditSession;
@@ -369,7 +385,6 @@ export function StudioApp() {
369
385
  const r = previewIframe.getBoundingClientRect();
370
386
  canvasRectRef.current = { left: r.left, top: r.top, width: r.width, height: r.height };
371
387
  }, [gestureState, previewIframe]);
372
-
373
388
  const handlePreviewIframeRef = useCallback(
374
389
  (iframe: HTMLIFrameElement | null) => {
375
390
  previewIframeRef.current = iframe;
@@ -410,7 +425,6 @@ export function StudioApp() {
410
425
  previewIframeRef,
411
426
  rightPanelTab: panelLayout.rightPanelTab,
412
427
  rightCollapsed: panelLayout.rightCollapsed,
413
- timelineVisible,
414
428
  activeCompPathHydrated,
415
429
  domEditSelection: domEditSession.domEditSelection,
416
430
  buildDomSelectionFromTarget: domEditSession.buildDomSelectionFromTarget,
@@ -438,18 +452,15 @@ export function StudioApp() {
438
452
  waitForPendingDomEditSaves: previewPersistence.waitForPendingDomEditSaves,
439
453
  handlePreviewIframeRef,
440
454
  refreshPreviewDocumentVersion,
441
- timelineVisible,
442
- toggleTimelineVisibility,
443
455
  });
444
456
  const timelineToolbar = useMemo(
445
457
  () => (
446
458
  <TimelineToolbar
447
- toggleTimelineVisibility={toggleTimelineVisibility}
448
459
  domEditSession={domEditSession}
449
460
  onSplitElement={timelineEditing.handleTimelineElementSplit}
450
461
  />
451
462
  ),
452
- [toggleTimelineVisibility, domEditSession, timelineEditing.handleTimelineElementSplit],
463
+ [domEditSession, timelineEditing.handleTimelineElementSplit],
453
464
  );
454
465
  if (resolving || waitingForServer || !projectId)
455
466
  return <StudioSplash waiting={waitingForServer} />;
@@ -494,78 +505,77 @@ export function StudioApp() {
494
505
  onSelectComposition={handleSelectComposition}
495
506
  />
496
507
  )}
497
- <div
498
- className={`flex flex-1 min-h-0${
499
- viewModeValue.viewMode === "storyboard" ? " hidden" : ""
500
- }`}
501
- >
502
- <StudioLeftSidebar
503
- leftSidebarRef={leftSidebarRef}
504
- onSelectComposition={handleSelectComposition}
505
- onAddBlock={handleAddBlock}
506
- onPreviewBlock={setBlockPreview}
507
- onLint={handleLint}
508
- linting={linting}
509
- lintFindingCount={lintModal?.length ?? findingsByFile.size}
510
- lintFindingsByFile={findingsByFile}
511
- />
512
- <StudioPreviewArea
513
- timelineToolbar={timelineToolbar}
514
- renderClipContent={renderClipContent}
515
- handleTimelineElementDelete={timelineEditing.handleTimelineElementDelete}
516
- handleTimelineAssetDrop={timelineEditing.handleTimelineAssetDrop}
517
- handleTimelineBlockDrop={handleTimelineBlockDrop}
518
- handlePreviewBlockDrop={handlePreviewBlockDrop}
519
- handleTimelineFileDrop={timelineEditing.handleTimelineFileDrop}
520
- handleTimelineElementMove={timelineEditing.handleTimelineElementMove}
521
- handleTimelineElementResize={timelineEditing.handleTimelineElementResize}
522
- handleTimelineGroupMove={timelineEditing.handleTimelineGroupMove}
523
- handleTimelineGroupResize={timelineEditing.handleTimelineGroupResize}
524
- handleToggleTrackHidden={timelineEditing.handleToggleTrackHidden}
525
- handleToggleElementHidden={timelineEditing.handleToggleElementHidden}
526
- handleBlockedTimelineEdit={timelineEditing.handleBlockedTimelineEdit}
527
- handleTimelineElementSplit={timelineEditing.handleTimelineElementSplit}
528
- handleRazorSplit={timelineEditing.handleRazorSplit}
529
- handleRazorSplitAll={timelineEditing.handleRazorSplitAll}
530
- setCompIdToSrc={setCompIdToSrc}
531
- setCompositionLoading={setCompositionLoading}
532
- shouldShowSelectedDomBounds={shouldShowSelectedDomBounds}
533
- isGestureRecording={gestureState === "recording"}
534
- recordingState={gestureState}
535
- onToggleRecording={recordingToggle}
536
- blockPreview={blockPreview}
537
- gestureOverlay={
538
- gestureState === "recording" && previewIframe ? (
539
- <GestureTrailOverlay
540
- samples={gestureRecording.samplesRef.current}
541
- sampleCount={gestureRecording.samplesRef.current.length}
542
- trail={gestureRecording.trailRef.current}
543
- canvasRect={canvasRectRef.current!}
544
- compositionSize={compositionDimensions ?? undefined}
545
- mode="recording"
546
- />
547
- ) : undefined
548
- }
549
- />
550
- {!panelLayout.rightCollapsed && (
551
- <StudioRightPanel
552
- designPanelActive={designPanelActive}
553
- activeBlockParams={activeBlockParams}
554
- onCloseBlockParams={() => {
555
- setActiveBlockParams(null);
556
- panelLayout.setRightPanelTab("design");
557
- }}
558
- recordingState={gestureState}
559
- recordingDuration={gestureRecording.recordingDuration}
560
- onToggleRecording={recordingToggle}
561
- sdkSession={sdkHandle.session}
562
- reloadPreview={reloadPreview}
563
- domEditSaveTimestampRef={domEditSaveTimestampRef}
564
- recordEdit={editHistory.recordEdit}
565
- onToggleElementHidden={timelineEditing.handleToggleElementHidden}
508
+ <EditorShell
509
+ hidden={viewModeValue.viewMode === "storyboard"}
510
+ left={
511
+ <StudioLeftSidebar
512
+ leftSidebarRef={leftSidebarRef}
513
+ onSelectComposition={handleSelectComposition}
514
+ onAddBlock={handleAddBlock}
515
+ onPreviewBlock={setBlockPreview}
516
+ onLint={handleLint}
517
+ linting={linting}
518
+ lintFindingCount={lintModal?.length ?? findingsByFile.size}
519
+ lintFindingsByFile={findingsByFile}
520
+ onAddAssetToTimeline={handleAddAssetAtPlayhead}
566
521
  />
567
- )}
568
- </div>
522
+ }
523
+ right={
524
+ panelLayout.rightCollapsed ? null : (
525
+ <StudioRightPanel
526
+ designPanelActive={designPanelActive}
527
+ activeBlockParams={activeBlockParams}
528
+ onCloseBlockParams={() => {
529
+ setActiveBlockParams(null);
530
+ panelLayout.setRightPanelTab("design");
531
+ }}
532
+ recordingState={gestureState}
533
+ recordingDuration={gestureRecording.recordingDuration}
534
+ onToggleRecording={recordingToggle}
535
+ sdkSession={sdkHandle.session}
536
+ reloadPreview={reloadPreview}
537
+ domEditSaveTimestampRef={domEditSaveTimestampRef}
538
+ recordEdit={editHistory.recordEdit}
539
+ onToggleElementHidden={timelineEditing.handleToggleElementHidden}
540
+ />
541
+ )
542
+ }
543
+ timelineToolbar={timelineToolbar}
544
+ renderClipContent={renderClipContent}
545
+ handleTimelineElementDelete={timelineEditing.handleTimelineElementDelete}
546
+ handleTimelineAssetDrop={timelineEditing.handleTimelineAssetDrop}
547
+ handleTimelineBlockDrop={handleTimelineBlockDrop}
548
+ handlePreviewBlockDrop={handlePreviewBlockDrop}
549
+ handleTimelineFileDrop={timelineEditing.handleTimelineFileDrop}
550
+ handleTimelineElementMove={timelineEditing.handleTimelineElementMove}
551
+ handleTimelineElementsMove={handleTimelineElementsMove}
552
+ handleTimelineElementResize={timelineEditing.handleTimelineElementResize}
553
+ handleTimelineGroupResize={timelineEditing.handleTimelineGroupResize}
554
+ handleToggleTrackHidden={timelineEditing.handleToggleTrackHidden}
555
+ handleBlockedTimelineEdit={timelineEditing.handleBlockedTimelineEdit}
556
+ handleTimelineElementSplit={timelineEditing.handleTimelineElementSplit}
557
+ handleRazorSplit={timelineEditing.handleRazorSplit}
558
+ handleRazorSplitAll={timelineEditing.handleRazorSplitAll}
559
+ setCompIdToSrc={setCompIdToSrc}
560
+ setCompositionLoading={setCompositionLoading}
561
+ shouldShowSelectedDomBounds={shouldShowSelectedDomBounds}
562
+ isGestureRecording={gestureState === "recording"}
563
+ recordingState={gestureState}
564
+ onToggleRecording={recordingToggle}
565
+ blockPreview={blockPreview}
566
+ gestureOverlay={
567
+ gestureState === "recording" && previewIframe ? (
568
+ <GestureTrailOverlay
569
+ samples={gestureRecording.samplesRef.current}
570
+ sampleCount={gestureRecording.samplesRef.current.length}
571
+ trail={gestureRecording.trailRef.current}
572
+ canvasRect={canvasRectRef.current!}
573
+ compositionSize={compositionDimensions ?? undefined}
574
+ mode="recording"
575
+ />
576
+ ) : undefined
577
+ }
578
+ />
569
579
  <StudioOverlays
570
580
  projectId={projectId}
571
581
  projectDir={fileManager.projectDir}
@@ -0,0 +1,253 @@
1
+ import { useCallback, type ReactNode } from "react";
2
+ import { PreviewPane } from "./nle/PreviewPane";
3
+ import { TimelinePane } from "./nle/TimelinePane";
4
+ import { PreviewOverlays } from "./nle/PreviewOverlays";
5
+ import {
6
+ useTimelineEditCallbacks,
7
+ type TimelineEditCallbackDeps,
8
+ } from "./nle/useTimelineEditCallbacks";
9
+ import { NLEProvider, useNLEContext } from "./nle/NLEContext";
10
+ import { CaptionTimeline } from "../captions/components/CaptionTimeline";
11
+ import { StudioFeedbackBar } from "./StudioFeedbackBar";
12
+ import { useStudioPlaybackContext, useStudioShellContext } from "../contexts/StudioContext";
13
+ import { useDomEditActionsContext } from "../contexts/DomEditContext";
14
+ import { TimelineEditProvider } from "../contexts/TimelineEditContext";
15
+ import type { TimelineElement } from "../player";
16
+ import type { BlockPreviewInfo } from "./sidebar/BlocksTab";
17
+ import type { GestureRecordingState } from "./editor/GestureRecordControl";
18
+
19
+ type RenderClipContent = (
20
+ element: TimelineElement,
21
+ style: { clip: string; label: string },
22
+ ) => ReactNode;
23
+ type TimelineDropPlacement = Pick<TimelineElement, "start" | "track">;
24
+
25
+ // The seven move/resize/split/razor handlers come from TimelineEditCallbackDeps
26
+ // (shared with useTimelineEditCallbacks); the rest are drop + wiring props.
27
+ export interface EditorShellProps extends TimelineEditCallbackDeps {
28
+ /** Left sidebar (media/library), rendered in the top row. */
29
+ left: ReactNode;
30
+ /** Right panel (inspector/design) or null when collapsed, in the top row. */
31
+ right: ReactNode;
32
+ /** Hide the whole shell (e.g. while the storyboard view is active). */
33
+ hidden?: boolean;
34
+ timelineToolbar: ReactNode;
35
+ renderClipContent: RenderClipContent;
36
+ handleTimelineElementDelete: (element: TimelineElement) => Promise<void> | void;
37
+ handleTimelineAssetDrop: (
38
+ assetPath: string,
39
+ placement: TimelineDropPlacement,
40
+ ) => Promise<void> | void;
41
+ handleTimelineBlockDrop?: (
42
+ blockName: string,
43
+ placement: TimelineDropPlacement,
44
+ ) => Promise<void> | void;
45
+ handlePreviewBlockDrop?: (
46
+ blockName: string,
47
+ position: { left: number; top: number },
48
+ ) => Promise<void> | void;
49
+ handleTimelineFileDrop: (
50
+ files: File[],
51
+ placement?: TimelineDropPlacement,
52
+ ) => Promise<void> | void;
53
+ setCompIdToSrc: (map: Map<string, string>) => void;
54
+ setCompositionLoading: (loading: boolean) => void;
55
+ shouldShowSelectedDomBounds: boolean;
56
+ blockPreview?: BlockPreviewInfo | null;
57
+ isGestureRecording?: boolean;
58
+ recordingState?: GestureRecordingState;
59
+ onToggleRecording?: () => void;
60
+ gestureOverlay?: ReactNode;
61
+ }
62
+
63
+ // The CapCut-style shell: [left | preview | right] in a top row, with a
64
+ // full-width timeline spanning the bottom. Owns the shared player +
65
+ // composition-stack state via NLEProvider so both rows share one player.
66
+ export function EditorShell({
67
+ left,
68
+ right,
69
+ hidden,
70
+ timelineToolbar,
71
+ renderClipContent,
72
+ handleTimelineElementDelete,
73
+ handleTimelineAssetDrop,
74
+ handleTimelineBlockDrop,
75
+ handlePreviewBlockDrop,
76
+ handleTimelineFileDrop,
77
+ handleTimelineElementMove,
78
+ handleTimelineElementsMove,
79
+ handleTimelineElementResize,
80
+ handleTimelineGroupResize,
81
+ handleToggleTrackHidden,
82
+ handleBlockedTimelineEdit,
83
+ handleTimelineElementSplit,
84
+ handleRazorSplit,
85
+ handleRazorSplitAll,
86
+ setCompIdToSrc,
87
+ setCompositionLoading,
88
+ shouldShowSelectedDomBounds,
89
+ isGestureRecording,
90
+ recordingState,
91
+ onToggleRecording,
92
+ blockPreview,
93
+ gestureOverlay,
94
+ }: EditorShellProps) {
95
+ const { projectId, activeCompPath, setActiveCompPath, handlePreviewIframeRef } =
96
+ useStudioShellContext();
97
+ const { refreshKey, captionEditMode, refreshPreviewDocumentVersion } = useStudioPlaybackContext();
98
+ const { handleTimelineElementSelect } = useDomEditActionsContext();
99
+
100
+ const timelineEditCallbacks = useTimelineEditCallbacks({
101
+ handleTimelineElementMove,
102
+ handleTimelineElementsMove,
103
+ handleTimelineElementResize,
104
+ handleTimelineGroupResize,
105
+ handleToggleTrackHidden,
106
+ handleBlockedTimelineEdit,
107
+ handleTimelineElementSplit,
108
+ handleRazorSplit,
109
+ handleRazorSplitAll,
110
+ });
111
+
112
+ return (
113
+ <div className={`flex flex-col flex-1 min-h-0${hidden ? " hidden" : ""}`}>
114
+ <TimelineEditProvider value={timelineEditCallbacks}>
115
+ <NLEProvider
116
+ projectId={projectId}
117
+ refreshKey={refreshKey}
118
+ activeCompositionPath={activeCompPath}
119
+ onIframeRef={handlePreviewIframeRef}
120
+ onCompIdToSrcChange={setCompIdToSrc}
121
+ onCompositionLoadingChange={setCompositionLoading}
122
+ onCompositionChange={(compPath) => {
123
+ // Sync activeCompPath when the user drills down via the timeline or
124
+ // navigates back — keeps sidebar + thumbnails in sync. Guard no-ops to
125
+ // avoid circular refresh cascades (activeCompPath → stack → onChange).
126
+ if (compPath !== activeCompPath) {
127
+ setActiveCompPath(compPath);
128
+ refreshPreviewDocumentVersion();
129
+ }
130
+ }}
131
+ >
132
+ <EditorShellBody
133
+ left={left}
134
+ right={right}
135
+ captionEditMode={captionEditMode}
136
+ onSelectTimelineElement={handleTimelineElementSelect}
137
+ onPreviewBlockDrop={handlePreviewBlockDrop}
138
+ timelineToolbar={timelineToolbar}
139
+ renderClipContent={renderClipContent}
140
+ onFileDrop={handleTimelineFileDrop}
141
+ onAssetDrop={handleTimelineAssetDrop}
142
+ onBlockDrop={handleTimelineBlockDrop}
143
+ onDeleteElement={handleTimelineElementDelete}
144
+ previewOverlay={
145
+ <PreviewOverlays
146
+ shouldShowSelectedDomBounds={shouldShowSelectedDomBounds}
147
+ blockPreview={blockPreview}
148
+ isGestureRecording={isGestureRecording}
149
+ recordingState={recordingState}
150
+ onToggleRecording={onToggleRecording}
151
+ gestureOverlay={gestureOverlay}
152
+ />
153
+ }
154
+ />
155
+ </NLEProvider>
156
+ </TimelineEditProvider>
157
+ <StudioFeedbackBar />
158
+ </div>
159
+ );
160
+ }
161
+
162
+ interface EditorShellBodyProps {
163
+ left: ReactNode;
164
+ right: ReactNode;
165
+ captionEditMode: boolean;
166
+ previewOverlay: ReactNode;
167
+ onSelectTimelineElement: (element: TimelineElement | null) => void;
168
+ onPreviewBlockDrop?: (
169
+ blockName: string,
170
+ position: { left: number; top: number },
171
+ ) => Promise<void> | void;
172
+ timelineToolbar: ReactNode;
173
+ renderClipContent: RenderClipContent;
174
+ onFileDrop: (files: File[], placement?: TimelineDropPlacement) => Promise<void> | void;
175
+ onAssetDrop: (assetPath: string, placement: TimelineDropPlacement) => Promise<void> | void;
176
+ onBlockDrop?: (blockName: string, placement: TimelineDropPlacement) => Promise<void> | void;
177
+ onDeleteElement: (element: TimelineElement) => Promise<void> | void;
178
+ }
179
+
180
+ function EditorShellBody({
181
+ left,
182
+ right,
183
+ captionEditMode,
184
+ previewOverlay,
185
+ onSelectTimelineElement,
186
+ onPreviewBlockDrop,
187
+ timelineToolbar,
188
+ renderClipContent,
189
+ onFileDrop,
190
+ onAssetDrop,
191
+ onBlockDrop,
192
+ onDeleteElement,
193
+ }: EditorShellBodyProps) {
194
+ const { compositionStack, updateCompositionStack, containerRef } = useNLEContext();
195
+
196
+ // Keyboard: Escape to pop composition level
197
+ const handleKeyDown = useCallback(
198
+ (e: React.KeyboardEvent) => {
199
+ if (e.key === "Escape" && compositionStack.length > 1) {
200
+ updateCompositionStack((prev) => prev.slice(0, -1));
201
+ }
202
+ },
203
+ [compositionStack.length, updateCompositionStack],
204
+ );
205
+
206
+ return (
207
+ <div
208
+ ref={containerRef}
209
+ // Shell canvas is a step LIGHTER than the near-black panel cards so the
210
+ // gaps between panels read as visible seams (CapCut-style).
211
+ className="flex flex-col flex-1 min-h-0 bg-[#18181B]"
212
+ onKeyDown={handleKeyDown}
213
+ tabIndex={-1}
214
+ >
215
+ {/* Top row: [left | preview | right] — outer padding + the 8px resize
216
+ seams give the panels CapCut-style separation on the dark canvas. */}
217
+ <div className="flex flex-row flex-1 min-h-0 px-px pt-px">
218
+ {left}
219
+ <div className="flex-1 min-w-0 flex flex-col relative">
220
+ <PreviewPane
221
+ previewOverlay={previewOverlay}
222
+ onSelectTimelineElement={onSelectTimelineElement}
223
+ onPreviewBlockDrop={onPreviewBlockDrop}
224
+ />
225
+ </div>
226
+ {right}
227
+ </div>
228
+
229
+ {/* Full-width timeline row */}
230
+ <TimelinePane
231
+ timelineToolbar={timelineToolbar}
232
+ renderClipContent={renderClipContent}
233
+ onFileDrop={onFileDrop}
234
+ onAssetDrop={onAssetDrop}
235
+ onBlockDrop={onBlockDrop}
236
+ onDeleteElement={onDeleteElement}
237
+ onSelectTimelineElement={onSelectTimelineElement}
238
+ timelineFooter={
239
+ captionEditMode ? (
240
+ <div className="border-t border-neutral-800/30 flex-shrink-0" style={{ height: 60 }}>
241
+ <div className="flex items-center gap-1.5 px-2 py-0.5">
242
+ <span className="text-[9px] font-medium text-neutral-500 uppercase tracking-wider">
243
+ Captions
244
+ </span>
245
+ </div>
246
+ <CaptionTimeline pixelsPerSecond={100} />
247
+ </div>
248
+ ) : undefined
249
+ }
250
+ />
251
+ </div>
252
+ );
253
+ }
@@ -17,9 +17,7 @@ export function StudioGlobalDragOverlay() {
17
17
  <polyline points="7 10 12 15 17 10" />
18
18
  <line x1="12" y1="15" x2="12" y2="3" />
19
19
  </svg>
20
- <span className="text-sm font-medium text-studio-accent">
21
- Drop files to import into project
22
- </span>
20
+ <span className="text-sm font-medium text-studio-accent">Drop to add at the playhead</span>
23
21
  </div>
24
22
  </div>
25
23
  );
@@ -18,6 +18,7 @@ export interface StudioLeftSidebarProps {
18
18
  linting: boolean;
19
19
  lintFindingCount?: number;
20
20
  lintFindingsByFile?: Map<string, { count: number; messages: string[] }>;
21
+ onAddAssetToTimeline?: (path: string) => void;
21
22
  }
22
23
 
23
24
  // fallow-ignore-next-line complexity
@@ -30,6 +31,7 @@ export function StudioLeftSidebar({
30
31
  linting,
31
32
  lintFindingCount,
32
33
  lintFindingsByFile,
34
+ onAddAssetToTimeline,
33
35
  }: StudioLeftSidebarProps) {
34
36
  const {
35
37
  leftCollapsed,
@@ -69,7 +71,7 @@ export function StudioLeftSidebar({
69
71
 
70
72
  if (leftCollapsed) {
71
73
  return (
72
- <div className="flex w-10 flex-shrink-0 flex-col items-center border-r border-neutral-800/50 bg-neutral-950 pt-1">
74
+ <div className="mr-0.5 flex w-10 flex-shrink-0 flex-col items-center rounded-lg border border-neutral-800/50 bg-neutral-950 pt-1">
73
75
  <button
74
76
  type="button"
75
77
  onClick={toggleLeftSidebar}
@@ -147,13 +149,18 @@ export function StudioLeftSidebar({
147
149
  onToggleCollapse={toggleLeftSidebar}
148
150
  onAddBlock={onAddBlock}
149
151
  onPreviewBlock={onPreviewBlock}
152
+ onAddAssetToTimeline={onAddAssetToTimeline}
150
153
  />
154
+ {/* Vertical resize divider: 3px visible seam, 8px pointer-capture zone via
155
+ the absolutely-positioned inner hit area. The outer element is w-[3px] so
156
+ it contributes only 3px of gap in the flex row; the inner -left-[2.5px]
157
+ element widens the hit area to 8px without affecting layout. */}
151
158
  <div
152
159
  role="separator"
153
160
  aria-label="Resize sidebar"
154
161
  aria-orientation="vertical"
155
162
  tabIndex={0}
156
- className="group w-2 flex-shrink-0 cursor-col-resize flex items-center justify-center outline-none focus-visible:bg-studio-accent/20"
163
+ className="group relative w-[3px] flex-shrink-0 cursor-col-resize outline-none focus-visible:bg-studio-accent/20"
157
164
  style={{ touchAction: "none" }}
158
165
  onPointerDown={(e) => handlePanelResizeStart("left", e)}
159
166
  onPointerMove={handlePanelResizeMove}
@@ -167,7 +174,10 @@ export function StudioLeftSidebar({
167
174
  setLeftWidth(Math.max(160, Math.min(maxLeft, leftWidth + delta)));
168
175
  }}
169
176
  >
170
- <div className="h-[52px] w-px bg-white/12 transition-colors group-hover:bg-white/18 group-active:bg-white/24" />
177
+ {/* Expanded hit zone: 8px wide, centered on the 3px seam */}
178
+ <div className="absolute inset-y-0 -left-[2.5px] w-2" />
179
+ {/* Visible hairline */}
180
+ <div className="absolute top-1/2 left-0 h-[52px] w-[3px] -translate-y-1/2 bg-white/12 transition-colors group-hover:bg-white/18 group-active:bg-white/24" />
171
181
  </div>
172
182
  </>
173
183
  );