@hyperframes/studio 0.7.54 → 0.7.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. package/dist/assets/{index-uBY329wb.js → index-BWnOxAiH.js} +1 -1
  2. package/dist/assets/{index-CMHYjEZ5.js → index-C4csZims.js} +1 -1
  3. package/dist/assets/index-CmVCjZjp.js +423 -0
  4. package/dist/assets/index-D-GyYi2d.css +1 -0
  5. package/dist/{chunk-SOTCF4DF.js → chunk-5QSIMBEJ.js} +2 -1
  6. package/dist/chunk-5QSIMBEJ.js.map +1 -0
  7. package/dist/{domEditingLayers-2ECJK24D.js → domEditingLayers-6LQGKPOI.js} +2 -2
  8. package/dist/index.d.ts +146 -144
  9. package/dist/index.html +2 -2
  10. package/dist/index.js +40582 -37803
  11. package/dist/index.js.map +1 -1
  12. package/package.json +7 -7
  13. package/src/App.tsx +106 -96
  14. package/src/components/EditorShell.tsx +253 -0
  15. package/src/components/StudioGlobalDragOverlay.tsx +1 -3
  16. package/src/components/StudioLeftSidebar.tsx +13 -3
  17. package/src/components/StudioRightPanel.tsx +8 -3
  18. package/src/components/TimelineToolbar.test.tsx +2 -2
  19. package/src/components/TimelineToolbar.tsx +179 -121
  20. package/src/components/editor/CanvasContextMenu.test.tsx +115 -0
  21. package/src/components/editor/CanvasContextMenu.tsx +198 -0
  22. package/src/components/editor/DomEditCropHandles.test.tsx +74 -3
  23. package/src/components/editor/DomEditCropHandles.tsx +64 -29
  24. package/src/components/editor/DomEditOverlay.test.ts +245 -279
  25. package/src/components/editor/DomEditOverlay.tsx +158 -126
  26. package/src/components/editor/DomEditOverlayHover.test.tsx +90 -0
  27. package/src/components/editor/DomEditRotateHandle.tsx +38 -18
  28. package/src/components/editor/DomEditSelectionChrome.test.tsx +80 -0
  29. package/src/components/editor/DomEditSelectionChrome.tsx +257 -0
  30. package/src/components/editor/LayersPanel.test.ts +66 -2
  31. package/src/components/editor/LayersPanel.tsx +43 -1
  32. package/src/components/editor/OffCanvasIndicators.test.tsx +34 -0
  33. package/src/components/editor/OffCanvasIndicators.tsx +49 -10
  34. package/src/components/editor/SnapGuideOverlay.tsx +28 -23
  35. package/src/components/editor/SnapToolbar.test.tsx +0 -1
  36. package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +214 -0
  37. package/src/components/editor/anchoredResizeReleaseShift.test.ts +208 -0
  38. package/src/components/editor/canvasContextMenuZOrder.test.ts +435 -0
  39. package/src/components/editor/canvasContextMenuZOrder.ts +352 -0
  40. package/src/components/editor/domEditNudge.test.ts +80 -0
  41. package/src/components/editor/domEditNudge.ts +44 -0
  42. package/src/components/editor/domEditOverlayGeometry.test.ts +167 -1
  43. package/src/components/editor/domEditOverlayGeometry.ts +280 -20
  44. package/src/components/editor/domEditOverlayGestures.ts +44 -41
  45. package/src/components/editor/domEditOverlayStartGesture.ts +62 -2
  46. package/src/components/editor/domEditResizeLocal.test.ts +131 -0
  47. package/src/components/editor/domEditResizeLocal.ts +125 -0
  48. package/src/components/editor/domEditingDom.ts +1 -1
  49. package/src/components/editor/manualEditsDomPatches.test.ts +47 -0
  50. package/src/components/editor/manualOffsetDrag.ts +55 -12
  51. package/src/components/editor/offCanvasIndicatorGeometry.ts +31 -10
  52. package/src/components/editor/offCanvasIndicatorRefresh.test.tsx +25 -1
  53. package/src/components/editor/resizeDraft.ts +108 -0
  54. package/src/components/editor/snapEngine.test.ts +12 -78
  55. package/src/components/editor/snapEngine.ts +13 -53
  56. package/src/components/editor/useDomEditNudge.test.tsx +228 -0
  57. package/src/components/editor/useDomEditNudge.ts +254 -0
  58. package/src/components/editor/useDomEditOverlayGestures.ts +115 -116
  59. package/src/components/editor/useDomEditOverlayRects.ts +9 -2
  60. package/src/components/nle/AssetPreviewOverlay.tsx +147 -0
  61. package/src/components/nle/NLEContext.test.ts +144 -0
  62. package/src/components/nle/NLEContext.tsx +328 -0
  63. package/src/components/nle/NLEPreview.tsx +1 -1
  64. package/src/components/nle/PreviewOverlays.tsx +240 -0
  65. package/src/components/nle/PreviewPane.tsx +163 -0
  66. package/src/components/nle/TimelinePane.test.ts +60 -0
  67. package/src/components/nle/TimelinePane.tsx +293 -0
  68. package/src/components/nle/TimelineResizeDivider.tsx +8 -2
  69. package/src/components/nle/useCompositionStack.test.tsx +44 -0
  70. package/src/components/nle/useCompositionStack.ts +17 -9
  71. package/src/components/nle/useTimelineEditCallbacks.ts +217 -0
  72. package/src/components/sidebar/AssetCard.tsx +326 -0
  73. package/src/components/sidebar/AssetContextMenu.tsx +15 -30
  74. package/src/components/sidebar/AssetsTab.test.ts +96 -0
  75. package/src/components/sidebar/AssetsTab.tsx +71 -206
  76. package/src/components/sidebar/AudioRow.tsx +39 -2
  77. package/src/components/sidebar/BlocksTab.tsx +8 -1
  78. package/src/components/sidebar/LeftSidebar.tsx +4 -1
  79. package/src/components/sidebar/assetHelpers.ts +29 -0
  80. package/src/contexts/DomEditContext.tsx +9 -0
  81. package/src/contexts/StudioContext.tsx +9 -8
  82. package/src/contexts/TimelineEditContext.tsx +3 -7
  83. package/src/hooks/deleteSelectedKeyframes.ts +35 -0
  84. package/src/hooks/domEditCommitTypes.ts +12 -1
  85. package/src/hooks/domSelectionTestHarness.ts +14 -0
  86. package/src/hooks/gestureTransaction.test.ts +311 -0
  87. package/src/hooks/gestureTransaction.ts +199 -0
  88. package/src/hooks/gsapDragCommit.test.ts +141 -41
  89. package/src/hooks/gsapDragCommit.ts +64 -74
  90. package/src/hooks/gsapDragStaticSetHelpers.ts +9 -21
  91. package/src/hooks/gsapResizeIntercept.test.ts +69 -0
  92. package/src/hooks/gsapResizeIntercept.ts +38 -10
  93. package/src/hooks/gsapRuntimeBridge.test.ts +49 -13
  94. package/src/hooks/gsapRuntimeBridge.ts +4 -4
  95. package/src/hooks/gsapScriptCommitTypes.ts +16 -5
  96. package/src/hooks/gsapShared.test.ts +19 -1
  97. package/src/hooks/gsapShared.ts +14 -0
  98. package/src/hooks/patchDocumentRootDuration.test.ts +71 -0
  99. package/src/hooks/timelineEditingGsap.ts +14 -0
  100. package/src/hooks/timelineEditingHelpers.test.ts +34 -1
  101. package/src/hooks/timelineEditingHelpers.ts +22 -48
  102. package/src/hooks/timelineMoveAdapter.test.ts +77 -0
  103. package/src/hooks/timelineMoveAdapter.ts +37 -0
  104. package/src/hooks/useAnimatedPropertyCommit.test.tsx +36 -8
  105. package/src/hooks/useAnimatedPropertyCommit.ts +31 -26
  106. package/src/hooks/useAppHotkeys.ts +16 -25
  107. package/src/hooks/useBlockHandlers.ts +55 -27
  108. package/src/hooks/useContextMenuDismiss.ts +31 -7
  109. package/src/hooks/useDomEditCommits.test.tsx +154 -1
  110. package/src/hooks/useDomEditCommits.ts +100 -3
  111. package/src/hooks/useDomEditPositionPatchCommit.ts +2 -0
  112. package/src/hooks/useDomEditSession.ts +2 -2
  113. package/src/hooks/useDomEditTextCommits.test.tsx +135 -0
  114. package/src/hooks/useDomEditTextCommits.ts +8 -3
  115. package/src/hooks/useDomEditWiring.ts +7 -9
  116. package/src/hooks/useDomGeometryCommits.ts +17 -2
  117. package/src/hooks/useDomSelection.test.ts +2 -36
  118. package/src/hooks/useDomSelection.ts +81 -51
  119. package/src/hooks/useDomSelectionSelectionGuards.test.ts +276 -0
  120. package/src/hooks/useElementLifecycleOps.test.tsx +258 -0
  121. package/src/hooks/useElementLifecycleOps.ts +40 -71
  122. package/src/hooks/useEnableKeyframes.test.ts +142 -1
  123. package/src/hooks/useEnableKeyframes.ts +73 -29
  124. package/src/hooks/useGestureCommit.test.tsx +130 -0
  125. package/src/hooks/useGestureCommit.ts +32 -8
  126. package/src/hooks/useGroupCommits.ts +7 -28
  127. package/src/hooks/useGsapAwareEditing.test.tsx +239 -0
  128. package/src/hooks/useGsapAwareEditing.ts +115 -20
  129. package/src/hooks/useGsapKeyframeOps.test.tsx +59 -0
  130. package/src/hooks/useGsapKeyframeOps.ts +38 -10
  131. package/src/hooks/useGsapScriptCommits.test.tsx +40 -1
  132. package/src/hooks/useGsapScriptCommits.ts +134 -50
  133. package/src/hooks/useGsapSelectionHandlers.ts +49 -16
  134. package/src/hooks/useMusicBeatAnalysis.ts +72 -36
  135. package/src/hooks/usePersistentEditHistory.test.ts +33 -0
  136. package/src/hooks/usePersistentEditHistory.ts +92 -58
  137. package/src/hooks/usePreviewPersistence.ts +25 -7
  138. package/src/hooks/useRazorSplit.history.test.tsx +303 -0
  139. package/src/hooks/useRazorSplit.test.ts +269 -0
  140. package/src/hooks/useRazorSplit.testHelpers.ts +64 -0
  141. package/src/hooks/useRazorSplit.ts +107 -36
  142. package/src/hooks/useRenderClipContent.ts +24 -6
  143. package/src/hooks/useStudioContextValue.ts +15 -5
  144. package/src/hooks/useStudioUrlState.ts +2 -4
  145. package/src/hooks/useTimelineEditing.test.tsx +20 -29
  146. package/src/hooks/useTimelineEditingTypes.ts +6 -0
  147. package/src/hooks/useTimelineGroupEditing.ts +18 -4
  148. package/src/index.ts +1 -1
  149. package/src/player/components/ImageThumbnail.test.tsx +173 -0
  150. package/src/player/components/ImageThumbnail.tsx +160 -0
  151. package/src/player/components/PlayheadIndicator.tsx +50 -5
  152. package/src/player/components/Timeline.test.ts +102 -19
  153. package/src/player/components/Timeline.tsx +194 -194
  154. package/src/player/components/TimelineCanvas.tsx +207 -521
  155. package/src/player/components/TimelineClip.tsx +4 -1
  156. package/src/player/components/TimelineClipDiamonds.tsx +9 -40
  157. package/src/player/components/TimelineEmptyState.tsx +3 -1
  158. package/src/player/components/TimelineLanes.tsx +487 -0
  159. package/src/player/components/TimelineOverlays.tsx +122 -0
  160. package/src/player/components/VideoThumbnail.test.tsx +152 -0
  161. package/src/player/components/VideoThumbnail.tsx +28 -7
  162. package/src/player/components/thumbnailUtils.test.ts +127 -0
  163. package/src/player/components/thumbnailUtils.ts +54 -0
  164. package/src/player/components/timelineCallbacks.ts +18 -18
  165. package/src/player/components/timelineClipChildren.tsx +39 -0
  166. package/src/player/components/timelineClipDragCommit.test.ts +943 -0
  167. package/src/player/components/timelineClipDragCommit.ts +401 -0
  168. package/src/player/components/timelineClipDragPreview.test.ts +144 -0
  169. package/src/player/components/timelineClipDragPreview.ts +323 -38
  170. package/src/player/components/timelineClipDragTypes.ts +65 -0
  171. package/src/player/components/timelineCollision.test.ts +477 -0
  172. package/src/player/components/timelineCollision.ts +263 -0
  173. package/src/player/components/timelineDragDrop.ts +69 -60
  174. package/src/player/components/timelineEditCapabilities.ts +52 -0
  175. package/src/player/components/timelineEditing.test.ts +1 -1
  176. package/src/player/components/timelineEditing.ts +76 -59
  177. package/src/player/components/timelineGroupEditing.test.ts +126 -0
  178. package/src/player/components/timelineGroupEditing.ts +133 -0
  179. package/src/player/components/timelineGroupResizeCommit.ts +54 -0
  180. package/src/player/components/timelineLayout.test.ts +95 -23
  181. package/src/player/components/timelineLayout.ts +120 -61
  182. package/src/player/components/timelineMarquee.test.ts +197 -0
  183. package/src/player/components/timelineMarquee.ts +112 -0
  184. package/src/player/components/timelineMultiDragPreview.test.ts +127 -0
  185. package/src/player/components/timelineMultiDragPreview.ts +106 -0
  186. package/src/player/components/timelineOptimisticRevision.ts +42 -0
  187. package/src/player/components/timelineSnapping.test.ts +134 -0
  188. package/src/player/components/timelineSnapping.ts +110 -0
  189. package/src/player/components/timelineStackingSync.test.ts +244 -0
  190. package/src/player/components/timelineStackingSync.ts +331 -0
  191. package/src/player/components/timelineTheme.ts +6 -1
  192. package/src/player/components/timelineZones.test.ts +425 -0
  193. package/src/player/components/timelineZones.ts +198 -0
  194. package/src/player/components/timelineZoom.test.ts +67 -0
  195. package/src/player/components/timelineZoom.ts +51 -0
  196. package/src/player/components/useResolvedTimelineEditCallbacks.ts +25 -3
  197. package/src/player/components/useTimelineClipDrag.resize.test.tsx +241 -0
  198. package/src/player/components/useTimelineClipDrag.ts +380 -386
  199. package/src/player/components/useTimelineEditPinning.ts +121 -0
  200. package/src/player/components/useTimelineGeometry.ts +129 -0
  201. package/src/player/components/useTimelinePlayhead.ts +4 -16
  202. package/src/player/components/useTimelineRangeSelection.ts +341 -50
  203. package/src/player/components/useTimelineStackingSync.test.tsx +67 -0
  204. package/src/player/components/useTimelineStackingSync.ts +83 -0
  205. package/src/player/hooks/useExpandedTimelineElements.ts +0 -3
  206. package/src/player/hooks/usePlaybackKeyboard.ts +5 -0
  207. package/src/player/hooks/useTimelinePlayer.ts +48 -53
  208. package/src/player/hooks/useTimelinePlayerLoop.ts +85 -0
  209. package/src/player/hooks/useTimelineSyncCallbacks.test.ts +219 -0
  210. package/src/player/hooks/useTimelineSyncCallbacks.ts +104 -4
  211. package/src/player/index.ts +3 -2
  212. package/src/player/lib/time.test.ts +19 -19
  213. package/src/player/lib/time.ts +6 -2
  214. package/src/player/lib/timelineDOM.test.ts +25 -98
  215. package/src/player/lib/timelineDOM.ts +12 -81
  216. package/src/player/lib/timelineElementHelpers.ts +54 -5
  217. package/src/player/lib/timelineIframeHelpers.ts +2 -0
  218. package/src/player/store/playerStore.ts +62 -3
  219. package/src/styles/studio.css +27 -0
  220. package/src/utils/assetClickBehavior.test.ts +104 -0
  221. package/src/utils/assetClickBehavior.ts +75 -0
  222. package/src/utils/assetPreviewStore.ts +33 -0
  223. package/src/utils/blockInstaller.ts +10 -2
  224. package/src/utils/canvasNudgeGate.test.ts +31 -0
  225. package/src/utils/canvasNudgeGate.ts +32 -0
  226. package/src/utils/editHistory.test.ts +35 -0
  227. package/src/utils/gsapSoftReload.test.ts +119 -1
  228. package/src/utils/gsapSoftReload.ts +153 -0
  229. package/src/utils/mediaTypes.ts +3 -2
  230. package/src/utils/resizeDebug.ts +55 -0
  231. package/src/utils/rootDuration.test.ts +90 -1
  232. package/src/utils/rootDuration.ts +76 -13
  233. package/src/utils/sdkCutover.ts +4 -0
  234. package/src/utils/studioFileHistory.ts +37 -7
  235. package/src/utils/studioHelpers.ts +62 -0
  236. package/src/utils/studioPreviewHelpers.test.ts +65 -8
  237. package/src/utils/studioPreviewHelpers.ts +10 -0
  238. package/src/utils/studioTelemetry.ts +4 -1
  239. package/src/utils/studioUiPreferences.test.ts +38 -0
  240. package/src/utils/studioUiPreferences.ts +27 -0
  241. package/src/utils/studioUrlState.test.ts +0 -1
  242. package/src/utils/timelineAssetDrop.ts +62 -0
  243. package/src/utils/timelineDiscovery.ts +0 -17
  244. package/src/utils/timelineInspector.test.ts +121 -0
  245. package/src/utils/timelineInspector.ts +32 -1
  246. package/dist/assets/index-Dq7FEg0K.css +0 -1
  247. package/dist/assets/index-pRhCpGPz.js +0 -423
  248. package/dist/chunk-SOTCF4DF.js.map +0 -1
  249. package/src/components/StudioPreviewArea.tsx +0 -500
  250. package/src/components/nle/NLELayout.test.ts +0 -12
  251. package/src/components/nle/NLELayout.tsx +0 -591
  252. package/src/player/components/TimelineLayerGutter.tsx +0 -61
  253. package/src/player/components/TimelineSelectionOverlays.tsx +0 -55
  254. package/src/player/components/timelineMarqueeSelection.test.ts +0 -87
  255. package/src/player/components/timelineSnapTargets.test.ts +0 -144
  256. package/src/player/components/timelineSnapTargets.ts +0 -164
  257. package/src/player/components/useTimelineClipDrag.test.tsx +0 -506
  258. package/src/player/components/useTimelineClipGroupDrag.ts +0 -417
  259. package/src/player/components/useTimelineMarqueeSelection.test.tsx +0 -231
  260. package/src/player/components/useTimelineMarqueeSelection.ts +0 -276
  261. package/src/utils/timelineDiscovery.test.ts +0 -90
  262. /package/dist/{domEditingLayers-2ECJK24D.js.map → domEditingLayers-6LQGKPOI.js.map} +0 -0
@@ -0,0 +1,217 @@
1
+ import { useCallback, useMemo } from "react";
2
+ import type { TimelineElement } from "../../player";
3
+ import { usePlayerStore } from "../../player/store/playerStore";
4
+ import type { BlockedTimelineEditIntent } from "../../player/components/timelineEditing";
5
+ import type { TimelineEditCallbacks } from "../../player/components/timelineCallbacks";
6
+ import { useStudioShellContext } from "../../contexts/StudioContext";
7
+ import {
8
+ useDomEditActionsContext,
9
+ useDomEditSelectionContext,
10
+ } from "../../contexts/DomEditContext";
11
+ import { resolveTweenStart, resolveTweenDuration } from "../../utils/globalTimeCompiler";
12
+ import { resolveClipTimingBasis } from "../../hooks/useGsapTweenCache";
13
+ import { resolveKeyframeRetime } from "../editor/keyframeRetime";
14
+ import type { TimelineMoveOperation } from "../../hooks/timelineMoveAdapter";
15
+
16
+ export interface TimelineEditCallbackDeps {
17
+ handleTimelineElementMove: (
18
+ element: TimelineElement,
19
+ updates: Pick<TimelineElement, "start" | "track">,
20
+ ) => Promise<void> | void;
21
+ handleTimelineElementsMove: (
22
+ edits: Array<{ element: TimelineElement; updates: Pick<TimelineElement, "start" | "track"> }>,
23
+ coalesceKey?: string,
24
+ operation?: TimelineMoveOperation,
25
+ ) => Promise<void> | void;
26
+ handleTimelineElementResize: (
27
+ element: TimelineElement,
28
+ updates: Pick<TimelineElement, "start" | "duration" | "playbackStart">,
29
+ ) => Promise<void> | void;
30
+ handleTimelineGroupResize: NonNullable<TimelineEditCallbacks["onResizeElements"]>;
31
+ handleToggleTrackHidden: (track: number, hidden: boolean) => Promise<void> | void;
32
+ handleBlockedTimelineEdit: (element: TimelineElement, intent: BlockedTimelineEditIntent) => void;
33
+ handleTimelineElementSplit: (element: TimelineElement, splitTime: number) => Promise<void> | void;
34
+ handleRazorSplit: (element: TimelineElement, splitTime: number) => Promise<void> | void;
35
+ handleRazorSplitAll: (splitTime: number) => Promise<void> | void;
36
+ }
37
+
38
+ /**
39
+ * Builds the timeline edit callback bag (move/resize/split/razor plus the
40
+ * keyframe-diamond callbacks) provided to `<Timeline>` via TimelineEditProvider.
41
+ * The keyframe callbacks resolve the dragged diamond back to its GSAP anim id +
42
+ * tween-relative percentage, reading DOM-edit selection state from context.
43
+ */
44
+ // fallow-ignore-next-line complexity
45
+ export function useTimelineEditCallbacks({
46
+ handleTimelineElementMove,
47
+ handleTimelineElementsMove,
48
+ handleTimelineElementResize,
49
+ handleTimelineGroupResize,
50
+ handleToggleTrackHidden,
51
+ handleBlockedTimelineEdit,
52
+ handleTimelineElementSplit,
53
+ handleRazorSplit,
54
+ handleRazorSplitAll,
55
+ }: TimelineEditCallbackDeps): TimelineEditCallbacks {
56
+ const { projectId, activeCompPath } = useStudioShellContext();
57
+ const { domEditSelection, selectedGsapAnimations } = useDomEditSelectionContext();
58
+ const {
59
+ handleGsapRemoveKeyframe,
60
+ handleGsapMoveKeyframeToPlayhead,
61
+ handleGsapMoveKeyframe,
62
+ handleGsapResizeKeyframedTween,
63
+ handleGsapUpdateMeta,
64
+ handleGsapAddKeyframe,
65
+ handleGsapConvertToKeyframes,
66
+ handleGsapRemoveAllKeyframes,
67
+ buildDomSelectionForTimelineElement,
68
+ } = useDomEditActionsContext();
69
+
70
+ // Resolve a timeline-diamond callback's clip-% to the keyframe's anim id + its
71
+ // tween-relative percentage (shared by the delete/move keyframe callbacks): the
72
+ // diamond reports a clip-% but the script ops key on the tween-%. Prefers the
73
+ // anim in the keyframe's property group, falling back to the first keyframed one.
74
+ const resolveKeyframeTarget = useCallback(
75
+ // fallow-ignore-next-line complexity
76
+ (pct: number): { animId: string; tweenPct: number } | null => {
77
+ const cached = usePlayerStore.getState().keyframeCache.get(domEditSelection?.id ?? "");
78
+ const kf = cached?.keyframes.find((k) => Math.abs(k.percentage - pct) < 0.2);
79
+ const group = kf?.propertyGroup;
80
+ const anim =
81
+ (group ? selectedGsapAnimations.find((a) => a.propertyGroup === group) : undefined) ??
82
+ selectedGsapAnimations.find((a) => a.keyframes);
83
+ return anim ? { animId: anim.id, tweenPct: kf?.tweenPercentage ?? pct } : null;
84
+ },
85
+ [domEditSelection?.id, selectedGsapAnimations],
86
+ );
87
+
88
+ return useMemo(
89
+ () => ({
90
+ onMoveElement: handleTimelineElementMove,
91
+ onMoveElements: handleTimelineElementsMove,
92
+ onResizeElement: handleTimelineElementResize,
93
+ onResizeElements: handleTimelineGroupResize,
94
+ onToggleTrackHidden: handleToggleTrackHidden,
95
+ onBlockedEditAttempt: handleBlockedTimelineEdit,
96
+ onSplitElement: handleTimelineElementSplit,
97
+ onRazorSplit: handleRazorSplit,
98
+ onRazorSplitAll: handleRazorSplitAll,
99
+ onDeleteAllKeyframes: () => {
100
+ // Hold the element where it is (collapse keyframes to a static set) rather
101
+ // than deleting the whole animation — deleting strands a stale GSAP base
102
+ // that the next drag adds to, flinging the element off-screen.
103
+ const anim = selectedGsapAnimations.find((a) => a.keyframes);
104
+ if (!anim) return;
105
+ handleGsapRemoveAllKeyframes(anim.id);
106
+ },
107
+ onDeleteKeyframe: (_elId: string, pct: number) => {
108
+ const target = resolveKeyframeTarget(pct);
109
+ if (target) handleGsapRemoveKeyframe(target.animId, target.tweenPct);
110
+ },
111
+ // Retime the keyframe to the playhead, preserving its value + ease.
112
+ onMoveKeyframeToPlayhead: (_elId: string, pct: number) => {
113
+ const target = resolveKeyframeTarget(pct);
114
+ if (target) handleGsapMoveKeyframeToPlayhead(target.animId, target.tweenPct);
115
+ },
116
+ // Drag-to-retime. The diamond reports clip-%s; resolveKeyframeTarget gives
117
+ // the dragged keyframe's anim + tween-%. We convert the clip-% drop to an
118
+ // absolute time (via the clip's timing basis) and let resolveKeyframeRetime
119
+ // decide: a drop inside the tween window is a plain move (re-key tween-%); a
120
+ // drop past the boundary (last keyframe past the end, first before the start)
121
+ // resizes the tween — position/duration grow so the dragged keyframe lands at
122
+ // the drop while every other keyframe keeps its absolute time (value+ease too).
123
+ // fallow-ignore-next-line complexity
124
+ onMoveKeyframe: (_elId: string, fromClipPct: number, toClipPct: number) => {
125
+ const target = resolveKeyframeTarget(fromClipPct);
126
+ const sel = domEditSelection;
127
+ if (!target || !sel) return;
128
+ const anim = selectedGsapAnimations.find((a) => a.id === target.animId);
129
+ const tweenStart = anim ? resolveTweenStart(anim) : null;
130
+ if (!anim || tweenStart === null) return;
131
+ const tweenDuration = anim.duration ?? resolveTweenDuration(anim);
132
+ const sourceFile = sel.sourceFile || activeCompPath || "index.html";
133
+ const { elements, domClipChildren } = usePlayerStore.getState();
134
+ const { elStart, elDuration } = resolveClipTimingBasis(
135
+ sel.id ?? "",
136
+ sourceFile,
137
+ elements,
138
+ domClipChildren,
139
+ );
140
+ const dropAbsTime = elStart + (toClipPct / 100) * elDuration;
141
+ const decision = resolveKeyframeRetime({
142
+ keyframes: anim.keyframes?.keyframes ?? [],
143
+ draggedTweenPct: target.tweenPct,
144
+ tweenStart,
145
+ tweenDuration,
146
+ dropAbsTime,
147
+ });
148
+ if (decision.kind === "move" && decision.toTweenPct != null) {
149
+ handleGsapMoveKeyframe(target.animId, target.tweenPct, decision.toTweenPct);
150
+ } else if (
151
+ decision.kind === "resize" &&
152
+ decision.pctRemap &&
153
+ decision.position != null &&
154
+ decision.duration != null
155
+ ) {
156
+ handleGsapResizeKeyframedTween(
157
+ target.animId,
158
+ decision.position,
159
+ decision.duration,
160
+ decision.pctRemap,
161
+ );
162
+ }
163
+ },
164
+ onChangeKeyframeEase: (_elId: string, _pct: number, ease: string) => {
165
+ for (const anim of selectedGsapAnimations) {
166
+ if (anim.keyframes) handleGsapUpdateMeta(anim.id, { ease });
167
+ }
168
+ },
169
+ // fallow-ignore-next-line complexity
170
+ onToggleKeyframeAtPlayhead: (el: TimelineElement) => {
171
+ const currentTime = usePlayerStore.getState().currentTime;
172
+ const pct =
173
+ el.duration > 0
174
+ ? Math.max(0, Math.min(100, Math.round(((currentTime - el.start) / el.duration) * 100)))
175
+ : 0;
176
+ const anim = selectedGsapAnimations.find((a) => a.keyframes);
177
+ if (anim?.keyframes) {
178
+ const existing = anim.keyframes.keyframes.find((k) => Math.abs(k.percentage - pct) <= 1);
179
+ if (existing) {
180
+ handleGsapRemoveKeyframe(anim.id, existing.percentage);
181
+ } else {
182
+ handleGsapAddKeyframe(anim.id, pct, "x", 0);
183
+ }
184
+ } else {
185
+ const flatAnim = selectedGsapAnimations.find((a) => !a.keyframes);
186
+ if (flatAnim) handleGsapConvertToKeyframes(flatAnim.id);
187
+ }
188
+ },
189
+ }),
190
+ // eslint-disable-next-line react-hooks/exhaustive-deps
191
+ [
192
+ handleTimelineElementMove,
193
+ handleTimelineElementsMove,
194
+ handleTimelineElementResize,
195
+ handleTimelineGroupResize,
196
+ handleToggleTrackHidden,
197
+ handleBlockedTimelineEdit,
198
+ handleTimelineElementSplit,
199
+ handleRazorSplit,
200
+ handleRazorSplitAll,
201
+ handleGsapRemoveAllKeyframes,
202
+ resolveKeyframeTarget,
203
+ selectedGsapAnimations,
204
+ handleGsapRemoveKeyframe,
205
+ handleGsapMoveKeyframeToPlayhead,
206
+ handleGsapMoveKeyframe,
207
+ handleGsapResizeKeyframedTween,
208
+ handleGsapUpdateMeta,
209
+ handleGsapAddKeyframe,
210
+ handleGsapConvertToKeyframes,
211
+ buildDomSelectionForTimelineElement,
212
+ projectId,
213
+ activeCompPath,
214
+ domEditSelection,
215
+ ],
216
+ );
217
+ }
@@ -0,0 +1,326 @@
1
+ /**
2
+ * AssetCard and FontRow — visual asset tile / row components for the Assets panel.
3
+ * Extracted from AssetsTab.tsx to keep that file under the 600-line CI gate.
4
+ */
5
+ import { useState, useEffect, useRef, useCallback } from "react";
6
+ import { VideoFrameThumbnail } from "../ui/VideoFrameThumbnail";
7
+ import { VIDEO_EXT, IMAGE_EXT } from "../../utils/mediaTypes";
8
+ import { TIMELINE_ASSET_MIME } from "../../utils/timelineAssetDrop";
9
+ import { ContextMenu } from "./AssetContextMenu";
10
+ import { usePlayerStore } from "../../player/store/playerStore";
11
+ import { useAssetPreviewStore } from "../../utils/assetPreviewStore";
12
+ import { findClipForAsset, isPointerClick } from "../../utils/assetClickBehavior";
13
+ import { basename, ext, truncateMiddle, formatDuration } from "./assetHelpers";
14
+ import { resolveMediaPreviewUrl } from "../../player/components/thumbnailUtils";
15
+
16
+ /** Drag payload writer shared by the asset tile and the font row: copy effect
17
+ * plus the timeline-asset MIME and a plain-text path fallback. */
18
+ function writeAssetDragData(e: React.DragEvent, asset: string): void {
19
+ e.dataTransfer.effectAllowed = "copy";
20
+ e.dataTransfer.setData(TIMELINE_ASSET_MIME, JSON.stringify({ path: asset }));
21
+ e.dataTransfer.setData("text/plain", asset);
22
+ }
23
+
24
+ /** Open the row/tile context menu at the pointer, shared by asset tile + font row. */
25
+ function openAssetContextMenu(
26
+ e: React.MouseEvent,
27
+ setContextMenu: (menu: { x: number; y: number }) => void,
28
+ ): void {
29
+ e.preventDefault();
30
+ setContextMenu({ x: e.clientX, y: e.clientY });
31
+ }
32
+
33
+ /**
34
+ * Lazily probe a video/audio URL for its duration via a hidden HTMLVideoElement
35
+ * (`preload="metadata"`). The manifest only covers ~/.media assets, so project
36
+ * assets in assets/ have no manifest entry — this fills the gap.
37
+ * Returns `undefined` until the probe completes; `null` if it failed.
38
+ */
39
+ function useProbedDuration(src: string, skip: boolean): number | null | undefined {
40
+ const [duration, setDuration] = useState<number | null | undefined>(undefined);
41
+ useEffect(() => {
42
+ if (skip) return;
43
+ let cancelled = false;
44
+ let retryTimer: ReturnType<typeof setTimeout> | undefined;
45
+ // The in-flight probe element, so unmount cleanup can abort its network
46
+ // fetch (clearing `src`) instead of leaving it to finish in the background.
47
+ let liveVid: HTMLVideoElement | null = null;
48
+
49
+ function teardown(vid: HTMLVideoElement) {
50
+ vid.onloadedmetadata = null;
51
+ vid.onerror = null;
52
+ vid.src = "";
53
+ }
54
+
55
+ function probe(attempt: number) {
56
+ if (cancelled) return;
57
+ const vid = document.createElement("video");
58
+ liveVid = vid;
59
+ vid.preload = "metadata";
60
+ vid.muted = true;
61
+ vid.onloadedmetadata = () => {
62
+ const d = Number.isFinite(vid.duration) && vid.duration > 0 ? vid.duration : null;
63
+ teardown(vid);
64
+ if (!cancelled) setDuration(d);
65
+ };
66
+ vid.onerror = () => {
67
+ teardown(vid);
68
+ if (!cancelled) {
69
+ if (attempt < 1) retryTimer = setTimeout(() => probe(attempt + 1), 50);
70
+ else setDuration(null);
71
+ }
72
+ };
73
+ vid.src = src;
74
+ }
75
+
76
+ probe(0);
77
+ return () => {
78
+ cancelled = true;
79
+ if (retryTimer) clearTimeout(retryTimer);
80
+ if (liveVid) teardown(liveVid);
81
+ };
82
+ }, [src, skip]);
83
+ return duration;
84
+ }
85
+
86
+ export interface AssetCardProps {
87
+ projectId: string;
88
+ asset: string;
89
+ used: boolean;
90
+ duration?: number;
91
+ onCopy: (path: string) => void;
92
+ isCopied: boolean;
93
+ onDelete?: (path: string) => void;
94
+ onRename?: (oldPath: string, newPath: string) => void;
95
+ onAddAssetToTimeline?: (path: string) => void;
96
+ }
97
+
98
+ /**
99
+ * Thumbnail card for images and video assets. Renders in a 2-col grid.
100
+ *
101
+ * Click behaviour (CapCut-style):
102
+ * - Already added → selects the clip on the timeline (setSelectedElementId).
103
+ * - Not yet added → opens the asset preview overlay over the canvas.
104
+ * Drag behaviour is preserved: a pointer movement exceeding DRAG_THRESHOLD_PX
105
+ * before pointerup is treated as drag-start, not a click.
106
+ */
107
+ // fallow-ignore-next-line complexity
108
+ export function AssetCard({
109
+ projectId,
110
+ asset,
111
+ used,
112
+ duration,
113
+ onCopy,
114
+ isCopied,
115
+ onDelete,
116
+ onRename,
117
+ onAddAssetToTimeline,
118
+ }: AssetCardProps) {
119
+ const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null);
120
+ const [hovered, setHovered] = useState(false);
121
+ const fullName = asset.split("/").pop() ?? asset;
122
+ const name = basename(asset);
123
+ const extension = ext(asset);
124
+ const serveUrl = resolveMediaPreviewUrl(asset, projectId);
125
+ const isVideo = VIDEO_EXT.test(asset);
126
+ const isImage = IMAGE_EXT.test(asset);
127
+ const probedDuration = useProbedDuration(serveUrl, !isVideo || duration != null);
128
+ const resolvedDuration = duration ?? probedDuration ?? undefined;
129
+ const durationLabel = formatDuration(resolvedDuration ?? 0);
130
+
131
+ // Drag-threshold click gate: track pointer-down position so we can ignore
132
+ // pointer-up events that followed a real drag gesture.
133
+ const pointerDownRef = useRef<{ x: number; y: number } | null>(null);
134
+
135
+ const setSelectedElementId = usePlayerStore((s) => s.setSelectedElementId);
136
+ const elements = usePlayerStore((s) => s.elements);
137
+ const setPreviewAsset = useAssetPreviewStore((s) => s.setPreviewAsset);
138
+
139
+ const handlePointerDown = useCallback((e: React.PointerEvent) => {
140
+ pointerDownRef.current = { x: e.clientX, y: e.clientY };
141
+ }, []);
142
+
143
+ const handlePointerUp = useCallback(
144
+ (e: React.PointerEvent) => {
145
+ const origin = pointerDownRef.current;
146
+ pointerDownRef.current = null;
147
+ if (!origin) return;
148
+ if (!isPointerClick(e.clientX - origin.x, e.clientY - origin.y)) return;
149
+ // Treat as click
150
+ if (used) {
151
+ const clip = findClipForAsset(elements, asset);
152
+ if (clip) {
153
+ setSelectedElementId(clip.key ?? clip.id);
154
+ return;
155
+ }
156
+ }
157
+ // Not added (or no matching clip found) → preview overlay
158
+ setPreviewAsset(asset, projectId);
159
+ },
160
+ [used, elements, asset, projectId, setSelectedElementId, setPreviewAsset],
161
+ );
162
+
163
+ return (
164
+ <>
165
+ <div
166
+ draggable
167
+ onPointerDown={handlePointerDown}
168
+ onPointerUp={handlePointerUp}
169
+ onDragStart={(e) => writeAssetDragData(e, asset)}
170
+ onContextMenu={(e) => openAssetContextMenu(e, setContextMenu)}
171
+ onPointerEnter={() => setHovered(true)}
172
+ onPointerLeave={() => setHovered(false)}
173
+ className={`flex flex-col gap-1 cursor-pointer rounded-md p-1 transition-colors ${
174
+ isCopied ? "bg-studio-accent/10" : "hover:bg-neutral-800/40"
175
+ }`}
176
+ >
177
+ {/* Thumbnail */}
178
+ <div className="w-full aspect-video rounded overflow-hidden bg-neutral-900 relative">
179
+ {isImage && (
180
+ <img
181
+ src={serveUrl}
182
+ alt={name}
183
+ loading="lazy"
184
+ className="w-full h-full object-cover"
185
+ onError={(e) => {
186
+ (e.target as HTMLImageElement).style.display = "none";
187
+ }}
188
+ />
189
+ )}
190
+ {isVideo && (
191
+ <>
192
+ <VideoFrameThumbnail src={serveUrl} />
193
+ {hovered && (
194
+ <video
195
+ src={serveUrl}
196
+ autoPlay
197
+ muted
198
+ loop
199
+ playsInline
200
+ className="absolute inset-0 w-full h-full object-cover"
201
+ />
202
+ )}
203
+ </>
204
+ )}
205
+ {!isImage && !isVideo && (
206
+ <div className="w-full h-full flex items-center justify-center">
207
+ <span className="text-[10px] font-medium text-neutral-600">{extension}</span>
208
+ </div>
209
+ )}
210
+
211
+ {/* "Added" badge — top-left */}
212
+ {used && (
213
+ <span className="absolute top-1 left-1 text-[9px] font-semibold leading-none px-1.5 py-[3px] rounded bg-neutral-950/80 text-panel-text-1">
214
+ Added
215
+ </span>
216
+ )}
217
+
218
+ {/* Duration badge — top-right, media only */}
219
+ {durationLabel && (
220
+ <span className="absolute top-1 right-1 text-[9px] font-medium leading-none px-1.5 py-[3px] rounded bg-neutral-950/80 text-panel-text-2 tabular-nums">
221
+ {durationLabel}
222
+ </span>
223
+ )}
224
+ </div>
225
+
226
+ {/* Filename caption */}
227
+ <span
228
+ className={`text-[10px] leading-tight text-center block w-full ${
229
+ used ? "text-panel-text-2" : "text-panel-text-4"
230
+ }`}
231
+ title={fullName}
232
+ >
233
+ {truncateMiddle(fullName, 22)}
234
+ </span>
235
+ </div>
236
+
237
+ {contextMenu && (
238
+ <ContextMenu
239
+ x={contextMenu.x}
240
+ y={contextMenu.y}
241
+ asset={asset}
242
+ onClose={() => setContextMenu(null)}
243
+ onCopy={onCopy}
244
+ onDelete={onDelete}
245
+ onRename={onRename}
246
+ onAddAtPlayhead={onAddAssetToTimeline}
247
+ />
248
+ )}
249
+ </>
250
+ );
251
+ }
252
+
253
+ export interface FontRowProps {
254
+ asset: string;
255
+ used: boolean;
256
+ onCopy: (path: string) => void;
257
+ isCopied: boolean;
258
+ onDelete?: (path: string) => void;
259
+ onRename?: (oldPath: string, newPath: string) => void;
260
+ onAddAssetToTimeline?: (path: string) => void;
261
+ }
262
+
263
+ /**
264
+ * Compact row for font assets (no meaningful thumbnail; show ext badge + name).
265
+ */
266
+ export function FontRow({
267
+ asset,
268
+ used,
269
+ onCopy,
270
+ isCopied,
271
+ onDelete,
272
+ onRename,
273
+ onAddAssetToTimeline,
274
+ }: FontRowProps) {
275
+ const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null);
276
+ const name = basename(asset);
277
+ const extension = ext(asset);
278
+
279
+ return (
280
+ <>
281
+ <div
282
+ draggable
283
+ onClick={() => onCopy(asset)}
284
+ onDragStart={(e) => writeAssetDragData(e, asset)}
285
+ onContextMenu={(e) => openAssetContextMenu(e, setContextMenu)}
286
+ className={`px-2.5 py-1.5 flex items-center gap-2.5 cursor-pointer transition-colors ${
287
+ isCopied
288
+ ? "bg-studio-accent/10 border-l-2 border-studio-accent"
289
+ : "border-l-2 border-transparent hover:bg-neutral-800/50"
290
+ }`}
291
+ >
292
+ <div className="w-[50px] h-[32px] rounded overflow-hidden bg-neutral-900 flex-shrink-0 flex items-center justify-center">
293
+ <span className="text-[9px] font-medium text-neutral-700">{extension}</span>
294
+ </div>
295
+ <div className="min-w-0 flex-1">
296
+ <span
297
+ className={`text-xs font-medium truncate block ${used ? "text-panel-text-1" : "text-panel-text-3"}`}
298
+ >
299
+ {name}
300
+ </span>
301
+ <div className="flex items-center gap-1.5">
302
+ <span className="text-[10px] text-neutral-600 truncate">{extension}</span>
303
+ {used && (
304
+ <span className="text-[9px] font-medium text-panel-accent bg-panel-accent/10 px-1.5 py-px rounded">
305
+ in use
306
+ </span>
307
+ )}
308
+ </div>
309
+ </div>
310
+ </div>
311
+
312
+ {contextMenu && (
313
+ <ContextMenu
314
+ x={contextMenu.x}
315
+ y={contextMenu.y}
316
+ asset={asset}
317
+ onClose={() => setContextMenu(null)}
318
+ onCopy={onCopy}
319
+ onDelete={onDelete}
320
+ onRename={onRename}
321
+ onAddAtPlayhead={onAddAssetToTimeline}
322
+ />
323
+ )}
324
+ </>
325
+ );
326
+ }
@@ -6,6 +6,7 @@ export function ContextMenu({
6
6
  onCopy,
7
7
  onDelete,
8
8
  onRename,
9
+ onAddAtPlayhead,
9
10
  }: {
10
11
  x: number;
11
12
  y: number;
@@ -14,6 +15,7 @@ export function ContextMenu({
14
15
  onCopy: (path: string) => void;
15
16
  onDelete?: (path: string) => void;
16
17
  onRename?: (oldPath: string, newPath: string) => void;
18
+ onAddAtPlayhead?: (path: string) => void;
17
19
  }) {
18
20
  return (
19
21
  <div
@@ -28,6 +30,19 @@ export function ContextMenu({
28
30
  className="absolute bg-neutral-900 border border-neutral-700 rounded-lg shadow-xl py-1 min-w-[140px] text-xs"
29
31
  style={{ left: x, top: y }}
30
32
  >
33
+ {onAddAtPlayhead && (
34
+ <button
35
+ type="button"
36
+ onClick={(e) => {
37
+ e.stopPropagation();
38
+ onAddAtPlayhead(asset);
39
+ onClose();
40
+ }}
41
+ className="w-full text-left px-3 py-1.5 text-neutral-300 hover:bg-neutral-800 transition-colors"
42
+ >
43
+ Add at playhead
44
+ </button>
45
+ )}
31
46
  <button
32
47
  onClick={(e) => {
33
48
  e.stopPropagation();
@@ -65,33 +80,3 @@ export function ContextMenu({
65
80
  </div>
66
81
  );
67
82
  }
68
-
69
- export function DeleteConfirm({
70
- name,
71
- onConfirm,
72
- onCancel,
73
- }: {
74
- name: string;
75
- onConfirm: () => void;
76
- onCancel: () => void;
77
- }) {
78
- return (
79
- <div className="px-2 py-1.5 bg-red-950/30 border-l-2 border-red-500 flex items-center justify-between gap-2">
80
- <span className="text-[10px] text-red-400 truncate">Delete {name}?</span>
81
- <div className="flex items-center gap-1 flex-shrink-0">
82
- <button
83
- onClick={onConfirm}
84
- className="px-2 py-0.5 text-[10px] rounded bg-red-600 text-white hover:bg-red-500 transition-colors"
85
- >
86
- Delete
87
- </button>
88
- <button
89
- onClick={onCancel}
90
- className="px-2 py-0.5 text-[10px] rounded text-neutral-400 hover:text-neutral-200 transition-colors"
91
- >
92
- Cancel
93
- </button>
94
- </div>
95
- </div>
96
- );
97
- }