@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,5 +1,6 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import { filterByUsage, countUsage, deriveUsedPaths } from "./AssetsTab";
3
+ import { truncateMiddle, formatDuration } from "./assetHelpers";
3
4
  import { globalAssetRows } from "./GlobalAssetsView";
4
5
 
5
6
  const assets = ["bgm.mp3", "logo.png", "orphan.wav"];
@@ -47,6 +48,39 @@ describe("deriveUsedPaths", () => {
47
48
  "assets/logo.png",
48
49
  ]);
49
50
  });
51
+
52
+ it("handles fully-absolute URLs produced by the core runtime (toAbsoluteAssetUrl)", () => {
53
+ // The runtime calls new URL(raw, document.baseURI).toString() which produces
54
+ // "http://localhost:3012/api/projects/demo/preview/assets/clip.mp4"
55
+ const used = deriveUsedPaths([
56
+ { src: "http://localhost:3012/api/projects/demo/preview/assets/clip.mp4" },
57
+ { src: "http://localhost:3012/api/projects/abc123/preview/assets/logo.png" },
58
+ ]);
59
+ expect(used.has("assets/clip.mp4")).toBe(true);
60
+ expect(used.has("assets/logo.png")).toBe(true);
61
+ expect(used.size).toBe(2);
62
+ });
63
+
64
+ it("decodes percent-encoded filenames (spaces, parens) so they match the asset list", () => {
65
+ // Files with spaces/parens: "assets/my file (1).mp4" authored in HTML
66
+ // → runtime resolves to "http://…/assets/my%20file%20(1).mp4"
67
+ const used = deriveUsedPaths([
68
+ { src: "http://localhost:3012/api/projects/p/preview/assets/my%20file%20(1).mp4" },
69
+ { src: "/api/projects/p/preview/assets/track%20one.mp3" },
70
+ ]);
71
+ expect(used.has("assets/my file (1).mp4")).toBe(true);
72
+ expect(used.has("assets/track one.mp3")).toBe(true);
73
+ expect(used.size).toBe(2);
74
+ });
75
+
76
+ it("round-trips: absolute URL with spaces matches filterByUsage against plain asset list", () => {
77
+ const used = deriveUsedPaths([
78
+ { src: "http://localhost:3012/api/projects/demo/preview/assets/my%20video.mp4" },
79
+ ]);
80
+ expect(filterByUsage(["assets/my video.mp4", "assets/other.png"], used, "used")).toEqual([
81
+ "assets/my video.mp4",
82
+ ]);
83
+ });
50
84
  });
51
85
 
52
86
  describe("countUsage", () => {
@@ -59,6 +93,68 @@ describe("countUsage", () => {
59
93
  });
60
94
  });
61
95
 
96
+ describe("truncateMiddle", () => {
97
+ it("returns the original string when it fits within maxLen", () => {
98
+ expect(truncateMiddle("short.mp4", 20)).toBe("short.mp4");
99
+ expect(truncateMiddle("exact_length_str.mp4", 20)).toBe("exact_length_str.mp4");
100
+ });
101
+
102
+ it("truncates longer strings with an ellipsis in the middle", () => {
103
+ const result = truncateMiddle("2a37eabf-long-uuid-887d8.mp4", 20);
104
+ expect(result.length).toBeLessThanOrEqual(20);
105
+ expect(result).toContain("…");
106
+ // Preserves head
107
+ expect(result.startsWith("2a37eabf-long-uuid-8")).toBe(false); // head is shortened
108
+ expect(result.startsWith("2a37eabf")).toBe(true);
109
+ // Preserves tail
110
+ expect(result.endsWith("887d8.mp4")).toBe(false); // tail portion only
111
+ expect(result.endsWith(".mp4")).toBe(true);
112
+ });
113
+
114
+ it("preserves the full filename extension in the tail", () => {
115
+ const result = truncateMiddle("verylongnamehere12345.mp4", 14);
116
+ expect(result.endsWith(".mp4")).toBe(true);
117
+ expect(result.length).toBeLessThanOrEqual(14);
118
+ });
119
+
120
+ it("handles maxLen of 1 (degenerate)", () => {
121
+ const result = truncateMiddle("abcdef", 1);
122
+ // head = 0, tail = 0 → just the ellipsis
123
+ expect(result).toBe("…");
124
+ });
125
+
126
+ it("handles a string of exactly maxLen+1 chars", () => {
127
+ const result = truncateMiddle("abcdefgh", 7);
128
+ expect(result.length).toBeLessThanOrEqual(7);
129
+ expect(result).toContain("…");
130
+ });
131
+ });
132
+
133
+ describe("formatDuration", () => {
134
+ it("formats whole seconds as MM:SS", () => {
135
+ expect(formatDuration(28)).toBe("00:28");
136
+ expect(formatDuration(60)).toBe("01:00");
137
+ expect(formatDuration(90)).toBe("01:30");
138
+ expect(formatDuration(3661)).toBe("61:01");
139
+ });
140
+
141
+ it("rounds fractional seconds to nearest whole", () => {
142
+ expect(formatDuration(28.4)).toBe("00:28");
143
+ expect(formatDuration(28.6)).toBe("00:29");
144
+ });
145
+
146
+ it("returns empty string for non-positive values", () => {
147
+ expect(formatDuration(0)).toBe("");
148
+ expect(formatDuration(-1)).toBe("");
149
+ });
150
+
151
+ it("returns empty string for non-finite values", () => {
152
+ expect(formatDuration(NaN)).toBe("");
153
+ expect(formatDuration(Infinity)).toBe("");
154
+ expect(formatDuration(-Infinity)).toBe("");
155
+ });
156
+ });
157
+
62
158
  describe("globalAssetRows", () => {
63
159
  const recs = [
64
160
  { id: "bgm_001", type: "bgm", description: "calm ambient" },
@@ -1,21 +1,12 @@
1
1
  // fallow-ignore-file code-duplication
2
2
  import { memo, useState, useCallback, useRef, useMemo, useEffect } from "react";
3
- import { VideoFrameThumbnail } from "../ui/VideoFrameThumbnail";
4
- import { MEDIA_EXT, IMAGE_EXT, VIDEO_EXT, FONT_EXT } from "../../utils/mediaTypes";
5
- import { TIMELINE_ASSET_MIME } from "../../utils/timelineAssetDrop";
3
+ import { MEDIA_EXT, FONT_EXT } from "../../utils/mediaTypes";
6
4
  import { copyTextToClipboard } from "../../utils/clipboard";
7
- import { ContextMenu } from "./AssetContextMenu";
8
5
  import { usePlayerStore } from "../../player/store/playerStore";
9
- import {
10
- type MediaCategory,
11
- getCategory,
12
- basename,
13
- ext,
14
- CATEGORY_LABELS,
15
- FILTER_ORDER,
16
- } from "./assetHelpers";
6
+ import { type MediaCategory, getCategory, CATEGORY_LABELS, FILTER_ORDER } from "./assetHelpers";
17
7
  import { AudioRow } from "./AudioRow";
18
8
  import { GlobalAssetsView } from "./GlobalAssetsView";
9
+ import { AssetCard, FontRow } from "./AssetCard";
19
10
 
20
11
  interface AssetsTabProps {
21
12
  projectId: string;
@@ -23,155 +14,7 @@ interface AssetsTabProps {
23
14
  onImport?: (files: FileList) => void;
24
15
  onDelete?: (path: string) => void;
25
16
  onRename?: (oldPath: string, newPath: string) => void;
26
- }
27
-
28
- // fallow-ignore-next-line complexity
29
- function ImageCard({
30
- projectId,
31
- asset,
32
- used,
33
- onCopy,
34
- isCopied,
35
- onDelete,
36
- onRename,
37
- size,
38
- }: {
39
- projectId: string;
40
- asset: string;
41
- used: boolean;
42
- onCopy: (path: string) => void;
43
- isCopied: boolean;
44
- onDelete?: (path: string) => void;
45
- onRename?: (oldPath: string, newPath: string) => void;
46
- size: "large" | "small";
47
- }) {
48
- const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null);
49
- const [hovered, setHovered] = useState(false);
50
- const name = basename(asset);
51
- const extension = ext(asset);
52
- const serveUrl = `/api/projects/${projectId}/preview/${asset}`;
53
- const isVideo = VIDEO_EXT.test(asset);
54
- const isImage = IMAGE_EXT.test(asset);
55
-
56
- const thumbW = size === "large" ? "w-full" : "w-[50px]";
57
- const thumbH = size === "large" ? "h-[100px]" : "h-[32px]";
58
-
59
- return (
60
- <>
61
- <div
62
- draggable
63
- onClick={() => onCopy(asset)}
64
- onDragStart={(e) => {
65
- e.dataTransfer.effectAllowed = "copy";
66
- e.dataTransfer.setData(TIMELINE_ASSET_MIME, JSON.stringify({ path: asset }));
67
- e.dataTransfer.setData("text/plain", asset);
68
- }}
69
- onContextMenu={(e) => {
70
- e.preventDefault();
71
- setContextMenu({ x: e.clientX, y: e.clientY });
72
- }}
73
- onPointerEnter={() => setHovered(true)}
74
- onPointerLeave={() => setHovered(false)}
75
- className={`transition-colors cursor-pointer ${
76
- size === "large"
77
- ? `px-2.5 py-1 ${isCopied ? "bg-studio-accent/10" : "hover:bg-neutral-800/30"}`
78
- : `px-2.5 py-1.5 flex items-center gap-2.5 ${
79
- isCopied
80
- ? "bg-studio-accent/10 border-l-2 border-studio-accent"
81
- : "border-l-2 border-transparent hover:bg-neutral-800/50"
82
- }`
83
- }`}
84
- >
85
- {size === "large" ? (
86
- <div className="flex flex-col gap-1">
87
- <div className={`${thumbW} ${thumbH} rounded overflow-hidden bg-neutral-900 relative`}>
88
- {isImage && (
89
- <img
90
- src={serveUrl}
91
- alt={name}
92
- loading="lazy"
93
- className="w-full h-full object-cover"
94
- onError={(e) => {
95
- (e.target as HTMLImageElement).style.display = "none";
96
- }}
97
- />
98
- )}
99
- {isVideo && <VideoFrameThumbnail src={serveUrl} />}
100
- {isVideo && hovered && (
101
- <video
102
- src={serveUrl}
103
- autoPlay
104
- muted
105
- loop
106
- playsInline
107
- className="absolute inset-0 w-full h-full object-cover"
108
- />
109
- )}
110
- </div>
111
- <div className="flex items-center gap-1.5">
112
- <span
113
- className={`text-xs font-medium truncate ${used ? "text-panel-text-1" : "text-panel-text-3"}`}
114
- >
115
- {name}
116
- </span>
117
- <span className="text-[10px] text-neutral-600">{extension}</span>
118
- {used && (
119
- <span className="text-[9px] font-medium text-panel-accent bg-panel-accent/10 px-1.5 py-px rounded">
120
- in use
121
- </span>
122
- )}
123
- </div>
124
- </div>
125
- ) : (
126
- <>
127
- <div className="w-[50px] h-[32px] rounded overflow-hidden bg-neutral-900 flex-shrink-0 flex items-center justify-center">
128
- {isImage && (
129
- <img
130
- src={serveUrl}
131
- alt={name}
132
- loading="lazy"
133
- className="w-full h-full object-cover"
134
- onError={(e) => {
135
- (e.target as HTMLImageElement).style.display = "none";
136
- }}
137
- />
138
- )}
139
- {!isImage && (
140
- <span className="text-[9px] font-medium text-neutral-700">{extension}</span>
141
- )}
142
- </div>
143
- <div className="min-w-0 flex-1">
144
- <span
145
- className={`text-xs font-medium truncate block ${used ? "text-panel-text-1" : "text-panel-text-3"}`}
146
- >
147
- {name}
148
- </span>
149
- <div className="flex items-center gap-1.5">
150
- <span className="text-[10px] text-neutral-600 truncate">{extension}</span>
151
- {used && (
152
- <span className="text-[9px] font-medium text-panel-accent bg-panel-accent/10 px-1.5 py-px rounded">
153
- in use
154
- </span>
155
- )}
156
- </div>
157
- </div>
158
- </>
159
- )}
160
- </div>
161
-
162
- {contextMenu && (
163
- <ContextMenu
164
- x={contextMenu.x}
165
- y={contextMenu.y}
166
- asset={asset}
167
- onClose={() => setContextMenu(null)}
168
- onCopy={onCopy}
169
- onDelete={onDelete}
170
- onRename={onRename}
171
- />
172
- )}
173
- </>
174
- );
17
+ onAddAssetToTimeline?: (path: string) => void;
175
18
  }
176
19
 
177
20
  export type UsageFilter = "all" | "used" | "unused";
@@ -200,20 +43,48 @@ export function countUsage(
200
43
  /**
201
44
  * Project-relative asset paths referenced by composition elements — the set the
202
45
  * "in use" badge, used-first sort, and usage filter all key on. Element src is
203
- * the raw authored value (timelineElementHelpers sets entry.src =
204
- * getAttribute("src")), so it can be a relative path ("assets/x.png"), a
205
- * "./"-prefixed path, the served "/api/projects/<id>/preview/assets/x.png" form,
206
- * or carry a ?query — normalize all of them to the bare project path so they
207
- * match the asset-list entries. Pure — unit-tested.
46
+ * populated from the core runtime's `resolveNodeAssetUrl` which calls
47
+ * `new URL(raw, document.baseURI).toString()`, turning authored relative paths
48
+ * into fully-absolute URLs with percent-encoded characters, e.g.
49
+ * "assets/my file (1).mp4"
50
+ * "http://localhost:3012/api/projects/demo/preview/assets/my%20file%20(1).mp4"
51
+ *
52
+ * This function normalizes every src shape to the bare project-relative path so
53
+ * it matches the asset-list entries:
54
+ * - Absolute URL → strip origin + /api/projects/<id>/preview/ prefix, decode %XX
55
+ * - Server-relative /api/…preview/… → same strip + decode
56
+ * - Relative "./"-prefixed or bare → strip leading ./ or /
57
+ * - ?query / #hash → dropped
58
+ *
59
+ * Pure — unit-tested.
208
60
  */
209
61
  export function deriveUsedPaths(elements: Array<{ src?: string }>): Set<string> {
210
62
  const paths = new Set<string>();
211
63
  for (const el of elements) {
212
64
  if (!el.src) continue;
213
- const s = el.src
65
+ let s = el.src;
66
+
67
+ // Strip absolute origin if present (http://host/path → /path)
68
+ try {
69
+ const u = new URL(s);
70
+ s = u.pathname + (u.search ? u.search : "") + (u.hash ? u.hash : "");
71
+ } catch {
72
+ // Not a valid absolute URL — leave as-is (relative path)
73
+ }
74
+
75
+ s = s
214
76
  .replace(/^\/api\/projects\/[^/]+\/preview\//, "") // strip the dev serve prefix
215
77
  .replace(/^\.?\//, "") // strip leading ./ or /
216
78
  .split(/[?#]/)[0]; // drop query / hash
79
+
80
+ // Decode percent-encoded characters (spaces, parens, etc.) so the path
81
+ // matches the plain-text asset-list entries the server returns.
82
+ try {
83
+ s = decodeURIComponent(s);
84
+ } catch {
85
+ // Malformed encoding — use as-is
86
+ }
87
+
217
88
  if (s) paths.add(s);
218
89
  }
219
90
  return paths;
@@ -225,6 +96,7 @@ export const AssetsTab = memo(function AssetsTab({
225
96
  onImport,
226
97
  onDelete,
227
98
  onRename,
99
+ onAddAssetToTimeline,
228
100
  }: AssetsTabProps) {
229
101
  const fileInputRef = useRef<HTMLInputElement>(null);
230
102
  const [dragOver, setDragOver] = useState(false);
@@ -232,17 +104,11 @@ export const AssetsTab = memo(function AssetsTab({
232
104
  const [activeFilter, setActiveFilter] = useState<MediaCategory | "all">("all");
233
105
  const [usageFilter, setUsageFilter] = useState<"all" | "used" | "unused">("all");
234
106
  const [searchQuery, setSearchQuery] = useState("");
235
- // Cross-project view: the global media-use cache (~/.media). The view itself
236
- // (GlobalAssetsView) owns its fetch — AssetsTab only tracks which scope is active.
237
107
  const [viewMode, setViewMode] = useState<"local" | "global">("local");
238
108
  const [manifest, setManifest] = useState<
239
109
  Map<string, { description?: string; duration?: number; width?: number; height?: number }>
240
110
  >(new Map());
241
111
 
242
- // Projects whose media manifest 404'd — most don't have one. Cache the miss so
243
- // we don't re-fetch (and spam the console) on every re-render; the effect was
244
- // also keyed on the `assets` array reference, which changes each render, so it
245
- // re-fired constantly. Key on a stable join + skip known-missing manifests.
246
112
  const manifest404Ref = useRef<Set<string>>(new Set());
247
113
  const assetsKey = assets.join("|");
248
114
  useEffect(() => {
@@ -278,7 +144,6 @@ export const AssetsTab = memo(function AssetsTab({
278
144
  cancelled = true;
279
145
  };
280
146
  }, [projectId, assetsKey]);
281
-
282
147
  const handleDrop = useCallback(
283
148
  (e: React.DragEvent) => {
284
149
  e.preventDefault();
@@ -287,7 +152,6 @@ export const AssetsTab = memo(function AssetsTab({
287
152
  },
288
153
  [onImport],
289
154
  );
290
-
291
155
  const handleCopyPath = useCallback(async (path: string) => {
292
156
  const copied = await copyTextToClipboard(path);
293
157
  if (copied) {
@@ -295,22 +159,27 @@ export const AssetsTab = memo(function AssetsTab({
295
159
  setTimeout(() => setCopiedPath(null), 1500);
296
160
  }
297
161
  }, []);
298
-
299
162
  const elements = usePlayerStore((s) => s.elements);
300
163
  const usedPaths = useMemo(() => deriveUsedPaths(elements), [elements]);
301
-
302
164
  const mediaAssets = useMemo(() => {
303
165
  const media = assets.filter((a) => MEDIA_EXT.test(a) || FONT_EXT.test(a));
304
166
  const all = filterByUsage(media, usedPaths, usageFilter);
305
167
  if (!searchQuery) return all;
306
168
  const q = searchQuery.toLowerCase();
307
169
  return all.filter((a) => {
308
- if (basename(a).toLowerCase().includes(q)) return true;
170
+ if (
171
+ a
172
+ .split("/")
173
+ .pop()
174
+ ?.replace(/\.[^.]*$/, "")
175
+ .toLowerCase()
176
+ .includes(q)
177
+ )
178
+ return true;
309
179
  const rec = manifest.get(a);
310
180
  return rec?.description?.toLowerCase().includes(q);
311
181
  });
312
182
  }, [assets, searchQuery, manifest, usageFilter, usedPaths]);
313
-
314
183
  const categorized = useMemo(() => {
315
184
  const groups: Record<MediaCategory, string[]> = { audio: [], images: [], video: [], fonts: [] };
316
185
  for (const a of mediaAssets) {
@@ -327,15 +196,11 @@ export const AssetsTab = memo(function AssetsTab({
327
196
  }
328
197
  return groups;
329
198
  }, [mediaAssets, usedPaths]);
330
-
331
199
  const counts = useMemo(() => {
332
200
  const c: Record<string, number> = { all: mediaAssets.length };
333
201
  for (const cat of FILTER_ORDER) c[cat] = categorized[cat].length;
334
202
  return c;
335
203
  }, [mediaAssets, categorized]);
336
-
337
- // Usage counts over the full media set (independent of the active usage filter,
338
- // so the chips don't show their own filtered totals).
339
204
  const usageCounts = useMemo(
340
205
  () =>
341
206
  countUsage(
@@ -344,12 +209,10 @@ export const AssetsTab = memo(function AssetsTab({
344
209
  ),
345
210
  [assets, usedPaths],
346
211
  );
347
-
348
212
  const visibleCategories =
349
213
  activeFilter === "all"
350
214
  ? FILTER_ORDER.filter((c) => categorized[c].length > 0)
351
215
  : [activeFilter as MediaCategory].filter((c) => categorized[c].length > 0);
352
-
353
216
  return (
354
217
  <div
355
218
  className={`flex-1 flex flex-col min-h-0 transition-colors ${dragOver ? "bg-studio-accent/[0.05]" : ""}`}
@@ -362,7 +225,7 @@ export const AssetsTab = memo(function AssetsTab({
362
225
  >
363
226
  {/* Header — matches design panel Section pattern */}
364
227
  <div className="px-4 pt-2.5 pb-1.5 flex-shrink-0">
365
- {/* Scope toggle — this project's assets vs the global media-use cache */}
228
+ {/* Scope toggle */}
366
229
  <div className="flex gap-1 mb-2.5 p-0.5 rounded-md bg-panel-input">
367
230
  {(["local", "global"] as const).map((m) => (
368
231
  <button
@@ -447,7 +310,7 @@ export const AssetsTab = memo(function AssetsTab({
447
310
  </div>
448
311
  )}
449
312
 
450
- {/* Filter chips — panel-input style (local view only) */}
313
+ {/* Filter chips */}
451
314
  {viewMode === "local" && mediaAssets.length > 0 && (
452
315
  <div className="flex gap-1.5 flex-wrap">
453
316
  <button
@@ -475,7 +338,6 @@ export const AssetsTab = memo(function AssetsTab({
475
338
  </button>
476
339
  ) : null,
477
340
  )}
478
- {/* Usage filter — show only assets the composition references, or only the unused ones */}
479
341
  {usageCounts.used > 0 && usageCounts.unused > 0 && (
480
342
  <>
481
343
  <span className="w-px self-stretch bg-panel-input mx-0.5" aria-hidden="true" />
@@ -505,7 +367,6 @@ export const AssetsTab = memo(function AssetsTab({
505
367
  )}
506
368
  </div>
507
369
 
508
- {/* Asset list */}
509
370
  <div className="flex-1 overflow-y-auto mt-1">
510
371
  {viewMode === "global" ? (
511
372
  <GlobalAssetsView searchQuery={searchQuery} />
@@ -553,34 +414,38 @@ export const AssetsTab = memo(function AssetsTab({
553
414
  isCopied={copiedPath === a}
554
415
  onDelete={onDelete}
555
416
  onRename={onRename}
417
+ onAddAssetToTimeline={onAddAssetToTimeline}
556
418
  />
557
419
  ))}
558
- {(cat === "images" || cat === "video") &&
559
- categorized[cat].map((a) => (
560
- <ImageCard
561
- key={a}
562
- projectId={projectId}
563
- asset={a}
564
- used={usedPaths.has(a)}
565
- onCopy={handleCopyPath}
566
- isCopied={copiedPath === a}
567
- onDelete={onDelete}
568
- onRename={onRename}
569
- size={categorized[cat].length <= 4 ? "large" : "small"}
570
- />
571
- ))}
420
+ {(cat === "images" || cat === "video") && (
421
+ <div className="grid grid-cols-2 gap-1 px-2 pb-1">
422
+ {categorized[cat].map((a) => (
423
+ <AssetCard
424
+ key={a}
425
+ projectId={projectId}
426
+ asset={a}
427
+ used={usedPaths.has(a)}
428
+ duration={manifest.get(a)?.duration}
429
+ onCopy={handleCopyPath}
430
+ isCopied={copiedPath === a}
431
+ onDelete={onDelete}
432
+ onRename={onRename}
433
+ onAddAssetToTimeline={onAddAssetToTimeline}
434
+ />
435
+ ))}
436
+ </div>
437
+ )}
572
438
  {cat === "fonts" &&
573
439
  categorized[cat].map((a) => (
574
- <ImageCard
440
+ <FontRow
575
441
  key={a}
576
- projectId={projectId}
577
442
  asset={a}
578
443
  used={usedPaths.has(a)}
579
444
  onCopy={handleCopyPath}
580
445
  isCopied={copiedPath === a}
581
446
  onDelete={onDelete}
582
447
  onRename={onRename}
583
- size="small"
448
+ onAddAssetToTimeline={onAddAssetToTimeline}
584
449
  />
585
450
  ))}
586
451
  </div>
@@ -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,36 @@ 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 elements = usePlayerStore((s) => s.elements);
47
+ const setPreviewAsset = useAssetPreviewStore((s) => s.setPreviewAsset);
48
+
49
+ const handlePointerDown = useCallback((e: React.PointerEvent) => {
50
+ pointerDownRef.current = { x: e.clientX, y: e.clientY };
51
+ }, []);
52
+
53
+ const handlePointerUp = useCallback(
54
+ (e: React.PointerEvent) => {
55
+ const origin = pointerDownRef.current;
56
+ pointerDownRef.current = null;
57
+ if (!origin) return;
58
+ if (!isPointerClick(e.clientX - origin.x, e.clientY - origin.y)) return;
59
+ if (used) {
60
+ const clip = findClipForAsset(elements, asset);
61
+ if (clip) {
62
+ setSelectedElementId(clip.key ?? clip.id);
63
+ return;
64
+ }
65
+ }
66
+ // Not added → preview overlay (audio player)
67
+ setPreviewAsset(asset, projectId);
68
+ },
69
+ [used, elements, asset, projectId, setSelectedElementId, setPreviewAsset],
70
+ );
36
71
 
37
72
  useEffect(() => {
38
73
  return () => {
@@ -109,7 +144,8 @@ export function AudioRow({
109
144
  <>
110
145
  <div
111
146
  draggable
112
- onClick={() => onCopy(asset)}
147
+ onPointerDown={handlePointerDown}
148
+ onPointerUp={handlePointerUp}
113
149
  onDragStart={(e) => {
114
150
  e.dataTransfer.effectAllowed = "copy";
115
151
  e.dataTransfer.setData(TIMELINE_ASSET_MIME, JSON.stringify({ path: asset }));
@@ -195,6 +231,7 @@ export function AudioRow({
195
231
  onCopy={onCopy}
196
232
  onDelete={onDelete}
197
233
  onRename={onRename}
234
+ onAddAtPlayhead={onAddAssetToTimeline}
198
235
  />
199
236
  )}
200
237
  </>
@@ -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" && (