@hyperframes/studio 0.8.49 → 0.8.51

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 (281) hide show
  1. package/dist/assets/{hyperframes-player-CqON2Rzo.js → hyperframes-player-Cn_PpHqP.js} +3 -3
  2. package/dist/assets/index-C05SNrvY.js +360 -0
  3. package/dist/assets/{index-mIgSsKqs.js → index-CPS3Cvj8.js} +1 -1
  4. package/dist/assets/{index-BGovDksE.js → index-Clrb11lj.js} +1 -1
  5. package/dist/assets/index-DUR9qzQM.css +1 -0
  6. package/dist/index.d.ts +29 -16
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +7283 -8606
  9. package/dist/index.js.map +1 -1
  10. package/dist/styles/tailwind-preset.js.map +1 -1
  11. package/package.json +11 -12
  12. package/src/App.tsx +132 -143
  13. package/src/captions/components/CaptionAnimationPanel.tsx +4 -4
  14. package/src/captions/components/CaptionOverlayUtils.ts +2 -3
  15. package/src/captions/components/CaptionPropertyPanel.tsx +3 -3
  16. package/src/captions/components/CaptionTimeline.tsx +1 -1
  17. package/src/captions/components/shared.tsx +2 -2
  18. package/src/components/AskAgentModal.tsx +5 -5
  19. package/src/components/EditorShell.selectionSync.test.tsx +4 -0
  20. package/src/components/EditorShell.tsx +28 -7
  21. package/src/components/ExternalFileConflictBanner.tsx +16 -16
  22. package/src/components/LintModal.tsx +6 -16
  23. package/src/components/MediaPreview.tsx +2 -2
  24. package/src/components/SaveQueuePausedBanner.tsx +2 -2
  25. package/src/components/StudioHeader.tsx +2 -62
  26. package/src/components/StudioLeftSidebar.tsx +7 -4
  27. package/src/components/StudioOverlays.tsx +2 -6
  28. package/src/components/StudioRightPanel.tsx +4 -4
  29. package/src/components/StudioToast.tsx +2 -2
  30. package/src/components/TimelineToolbar.tsx +7 -7
  31. package/src/components/dock/Dock.test.tsx +192 -0
  32. package/src/components/dock/Dock.tsx +264 -0
  33. package/src/components/dock/dock.css +23 -0
  34. package/src/components/dock/dockLayout.ts +58 -0
  35. package/src/components/dock/dockLayoutSchema.ts +66 -0
  36. package/src/components/dock/dockLayoutStore.ts +91 -0
  37. package/src/components/dock/panelRegistry.ts +67 -0
  38. package/src/components/editor/AnimationCard.tsx +4 -4
  39. package/src/components/editor/AnimationCardParts.tsx +6 -6
  40. package/src/components/editor/BlockParamsPanel.tsx +4 -4
  41. package/src/components/editor/BorderRadiusEditor.tsx +1 -1
  42. package/src/components/editor/CanvasContextMenu.tsx +1 -1
  43. package/src/components/editor/ComputedTweenNotice.tsx +1 -1
  44. package/src/components/editor/DomEditOverlay.tsx +1 -1
  45. package/src/components/editor/EaseCurveSection.tsx +1 -1
  46. package/src/components/editor/EaseParamFields.tsx +5 -5
  47. package/src/components/editor/FileTree.tsx +4 -4
  48. package/src/components/editor/FileTreeIcons.tsx +1 -1
  49. package/src/components/editor/FileTreeNodes.tsx +13 -13
  50. package/src/components/editor/InlineTextToolbar.tsx +1 -1
  51. package/src/components/editor/InspectorHeaderActions.tsx +3 -3
  52. package/src/components/editor/KeyframeDiamond.tsx +1 -1
  53. package/src/components/editor/KeyframeEaseList.tsx +1 -1
  54. package/src/components/editor/LayersPanel.tsx +3 -3
  55. package/src/components/editor/MarqueeOverlay.tsx +1 -1
  56. package/src/components/editor/OffCanvasIndicators.tsx +1 -1
  57. package/src/components/editor/PreviewGuides.test.tsx +88 -0
  58. package/src/components/editor/PreviewGuides.tsx +125 -0
  59. package/src/components/editor/PromotableControl.tsx +2 -2
  60. package/src/components/editor/PropertyPanelEmptyState.tsx +7 -7
  61. package/src/components/editor/PropertyPanelFlatHeader.tsx +2 -2
  62. package/src/components/editor/SnapToolbar.test.tsx +21 -0
  63. package/src/components/editor/SnapToolbar.tsx +27 -3
  64. package/src/components/editor/TimelineFxPopover.tsx +1 -1
  65. package/src/components/editor/TopologyLens.tsx +4 -4
  66. package/src/components/editor/Transform3DCube.tsx +2 -2
  67. package/src/components/editor/audioFxRevealTarget.ts +1 -0
  68. package/src/components/editor/domEditing.test.ts +1 -1
  69. package/src/components/editor/domEditingAgentPrompt.test.ts +8 -6
  70. package/src/components/editor/domEditingAgentPrompt.ts +9 -3
  71. package/src/components/editor/previewGuidesStore.ts +26 -0
  72. package/src/components/editor/propertyPanelColor.tsx +11 -11
  73. package/src/components/editor/propertyPanelColorCurveGraph.tsx +1 -1
  74. package/src/components/editor/propertyPanelColorGradingControls.tsx +7 -9
  75. package/src/components/editor/propertyPanelColorGradingSection.tsx +6 -6
  76. package/src/components/editor/propertyPanelColorGradingSlider.tsx +5 -5
  77. package/src/components/editor/propertyPanelColorWheels.tsx +1 -1
  78. package/src/components/editor/propertyPanelCommitField.tsx +1 -1
  79. package/src/components/editor/propertyPanelFill.tsx +4 -4
  80. package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +3 -3
  81. package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +7 -7
  82. package/src/components/editor/propertyPanelFlatEffectsSection.tsx +4 -4
  83. package/src/components/editor/propertyPanelFlatMediaSection.tsx +3 -3
  84. package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +1 -1
  85. package/src/components/editor/propertyPanelFlatPrimitives.tsx +10 -10
  86. package/src/components/editor/propertyPanelFlatSelectRow.tsx +3 -3
  87. package/src/components/editor/propertyPanelFlatTextSection.test.tsx +8 -9
  88. package/src/components/editor/propertyPanelFlatTextSection.tsx +5 -5
  89. package/src/components/editor/propertyPanelFlatToggle.tsx +1 -1
  90. package/src/components/editor/propertyPanelFont.tsx +5 -5
  91. package/src/components/editor/propertyPanelFxCarveModule.tsx +1 -1
  92. package/src/components/editor/propertyPanelFxControls.test.tsx +1 -1
  93. package/src/components/editor/propertyPanelFxControls.tsx +6 -6
  94. package/src/components/editor/propertyPanelFxEqModule.tsx +2 -2
  95. package/src/components/editor/propertyPanelFxPresetStyle.ts +2 -2
  96. package/src/components/editor/propertyPanelGradingNumberField.tsx +1 -1
  97. package/src/components/editor/propertyPanelMediaSection.tsx +1 -1
  98. package/src/components/editor/propertyPanelPrimitives.tsx +7 -7
  99. package/src/components/editor/propertyPanelSections.tsx +8 -8
  100. package/src/components/feedback/StudioFeedbackCard.tsx +7 -7
  101. package/src/components/feedback/feedbackTrigger.ts +0 -1
  102. package/src/components/nle/AssetPreviewOverlay.tsx +6 -2
  103. package/src/components/nle/CompositionBreadcrumb.tsx +3 -3
  104. package/src/components/nle/NLEPreview.test.ts +13 -0
  105. package/src/components/nle/NLEPreview.tsx +12 -7
  106. package/src/components/nle/PreviewOverlays.tsx +6 -6
  107. package/src/components/nle/PreviewPane.test.tsx +52 -0
  108. package/src/components/nle/PreviewPane.tsx +4 -2
  109. package/src/components/nle/TimelinePane.tsx +17 -3
  110. package/src/components/nle/TimelineResizeDivider.tsx +2 -2
  111. package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +1 -1
  112. package/src/components/nle/useCanvasZOrderTimelineMirror.ts +3 -3
  113. package/src/components/panels/SlideshowPanel.tsx +2 -2
  114. package/src/components/panels/SlideshowSubPanels.tsx +20 -17
  115. package/src/components/panels/VariablesBindElement.tsx +2 -2
  116. package/src/components/panels/VariablesDeclarationForm.tsx +2 -2
  117. package/src/components/panels/VariablesOtherCompositions.tsx +1 -1
  118. package/src/components/panels/VariablesPanel.tsx +3 -3
  119. package/src/components/panels/VariablesValueControls.tsx +2 -2
  120. package/src/components/renders/FfmpegRequiredNotice.tsx +5 -5
  121. package/src/components/renders/RenderQueue.test.tsx +7 -0
  122. package/src/components/renders/RenderQueue.tsx +6 -6
  123. package/src/components/renders/RenderQueueItem.tsx +6 -6
  124. package/src/components/sidebar/AssetCard.tsx +8 -8
  125. package/src/components/sidebar/AssetContextMenu.tsx +8 -8
  126. package/src/components/sidebar/AssetsTab.tsx +2 -2
  127. package/src/components/sidebar/AudioRow.tsx +6 -6
  128. package/src/components/sidebar/BlocksTab.tsx +6 -6
  129. package/src/components/sidebar/CompositionsTab.tsx +5 -5
  130. package/src/components/sidebar/GlobalAssetsView.tsx +1 -1
  131. package/src/components/sidebar/LeftSidebar.lintBadge.test.tsx +58 -0
  132. package/src/components/sidebar/LeftSidebar.tsx +19 -5
  133. package/src/components/sidebar/PromptPreviewModal.tsx +3 -3
  134. package/src/components/ui/Button.test.tsx +60 -0
  135. package/src/components/ui/Button.tsx +4 -4
  136. package/src/components/ui/SearchInput.tsx +2 -2
  137. package/src/components/ui/Tooltip.tsx +1 -1
  138. package/src/hooks/appHotkeysDispatch.ts +11 -0
  139. package/src/hooks/timelineAudioGroupCreate.ts +2 -2
  140. package/src/hooks/timelineTrackVisibility.ts +8 -11
  141. package/src/hooks/useAppHotkeys.textEditing.test.tsx +1 -0
  142. package/src/hooks/useAppHotkeys.ts +4 -1
  143. package/src/hooks/useAskAgentModal.ts +2 -0
  144. package/src/hooks/useClipboard.test.ts +234 -0
  145. package/src/hooks/useClipboard.ts +264 -70
  146. package/src/hooks/useGroupCommits.test.ts +0 -1
  147. package/src/hooks/useLintModal.test.tsx +65 -0
  148. package/src/hooks/useLintModal.ts +3 -0
  149. package/src/hooks/useRazorSplit.history.test.tsx +0 -1
  150. package/src/hooks/useRazorSplit.test.ts +0 -2
  151. package/src/hooks/useRazorSplit.test.tsx +0 -1
  152. package/src/hooks/useSdkSession.lifecycle.test.tsx +49 -4
  153. package/src/hooks/useSdkSession.ts +43 -10
  154. package/src/hooks/useStudioContextValue.ts +5 -19
  155. package/src/hooks/useTimelineAssetDropOps.test.tsx +41 -1
  156. package/src/hooks/useTimelineAssetDropOps.ts +52 -11
  157. package/src/hooks/useTimelineEditing.test.tsx +0 -1
  158. package/src/main.tsx +9 -3
  159. package/src/player/components/AudioWaveform.tsx +1 -1
  160. package/src/player/components/AutomationSelectionMenu.tsx +2 -2
  161. package/src/player/components/ClipContextMenu.tsx +90 -10
  162. package/src/player/components/CompositionThumbnail.test.ts +0 -3
  163. package/src/player/components/CompositionThumbnail.tsx +3 -4
  164. package/src/player/components/EditModal.tsx +1 -1
  165. package/src/player/components/ImageThumbnail.tsx +1 -1
  166. package/src/player/components/KeyframeDiamondContextMenu.tsx +4 -4
  167. package/src/player/components/LayerDisclosureRow.tsx +2 -2
  168. package/src/player/components/PlayerControls.tsx +1 -1
  169. package/src/player/components/ShortcutsPanel.tsx +9 -9
  170. package/src/player/components/SpeedMenu.tsx +3 -5
  171. package/src/player/components/Timeline.tsx +16 -15
  172. package/src/player/components/TimelineCanvas.tsx +45 -8
  173. package/src/player/components/TimelineDiamondConnectors.tsx +1 -1
  174. package/src/player/components/TimelineEmptyState.tsx +5 -5
  175. package/src/player/components/TimelineFxButton.tsx +6 -6
  176. package/src/player/components/TimelineGestureOverlay.test.tsx +35 -1
  177. package/src/player/components/TimelineGestureOverlay.tsx +1 -1
  178. package/src/player/components/TimelineGroupHeader.tsx +5 -5
  179. package/src/player/components/TimelineGroupLaneLabels.tsx +1 -1
  180. package/src/player/components/TimelineGroupRow.tsx +1 -1
  181. package/src/player/components/TimelineLanes.test.tsx +54 -1
  182. package/src/player/components/TimelineLanes.tsx +2 -5
  183. package/src/player/components/TimelineLayerGroupHeader.tsx +2 -2
  184. package/src/player/components/TimelineOverlays.tsx +12 -0
  185. package/src/player/components/TimelineRuler.tsx +1 -1
  186. package/src/player/components/TimelineShortcutHint.tsx +1 -1
  187. package/src/player/components/TimelineTrackHeader.tsx +1 -1
  188. package/src/player/components/TimelineTrackPlainHeader.tsx +3 -3
  189. package/src/player/components/TimelineTypes.ts +6 -1
  190. package/src/player/components/TrackGapContextMenu.tsx +1 -1
  191. package/src/player/components/VideoThumbnail.tsx +4 -2
  192. package/src/player/components/VolumeControl.tsx +2 -2
  193. package/src/player/components/timelineAuthoredMoveTarget.ts +1 -2
  194. package/src/player/components/timelineCallbacks.ts +8 -8
  195. package/src/player/components/timelineClipChildren.test.ts +2 -2
  196. package/src/player/components/timelineClipChildren.tsx +2 -1
  197. package/src/player/components/timelineClipDragCommit.ts +2 -2
  198. package/src/player/components/timelineClipDragPreview.ts +51 -27
  199. package/src/player/components/timelineClipDragTypes.ts +3 -0
  200. package/src/player/components/timelineDragDrop.test.tsx +92 -1
  201. package/src/player/components/timelineDragDrop.ts +82 -33
  202. package/src/player/components/timelineEditCapabilities.ts +1 -2
  203. package/src/player/components/timelineEditing.test.ts +0 -16
  204. package/src/player/components/timelineGroupEditing.test.ts +1 -3
  205. package/src/player/components/timelineGroupEditing.ts +0 -1
  206. package/src/player/components/timelineLaneProps.ts +3 -0
  207. package/src/player/components/timelineSnapping.test.ts +27 -0
  208. package/src/player/components/timelineSnapping.ts +14 -1
  209. package/src/player/components/timelineTrackDisplay.ts +1 -1
  210. package/src/player/components/timelineTrackPersistPipeline.test.ts +0 -53
  211. package/src/player/components/trackHeaderLabelRows.tsx +4 -4
  212. package/src/player/components/useTimelineClipContextMenu.ts +32 -0
  213. package/src/player/components/useTimelineClipDrag.resize.test.tsx +136 -2
  214. package/src/player/components/useTimelineClipDrag.ts +30 -3
  215. package/src/player/components/useTimelineTrackDerivations.ts +1 -1
  216. package/src/player/hooks/timelineDomBinding.test.ts +0 -3
  217. package/src/player/hooks/timelineSyncHydration.test.ts +18 -0
  218. package/src/player/hooks/timelineSyncHydration.ts +37 -18
  219. package/src/player/hooks/useTimelinePlayer.test.ts +3 -20
  220. package/src/player/hooks/useTimelineRowElements.test.ts +28 -0
  221. package/src/player/hooks/useTimelineRowElements.ts +17 -0
  222. package/src/player/hooks/useTimelineSyncCallbacks.emptyManifest.test.tsx +41 -0
  223. package/src/player/hooks/useTimelineSyncCallbacks.ts +9 -10
  224. package/src/player/lib/describeClips.test.ts +95 -0
  225. package/src/player/lib/describeClips.ts +52 -0
  226. package/src/player/lib/timelineDOM.test.ts +0 -80
  227. package/src/player/lib/timelineDOM.ts +1 -61
  228. package/src/player/lib/timelineElementHelpers.ts +2 -53
  229. package/src/player/store/playerStore.ts +5 -0
  230. package/src/player/store/timelineElement.ts +1 -3
  231. package/src/shims/canvasBrowserStub.js +50 -0
  232. package/src/styles/iconTokens.ts +20 -0
  233. package/src/styles/palette-reset.css +6 -0
  234. package/src/styles/studio.css +24 -21
  235. package/src/styles/tailwind-preset.shared.js +3 -0
  236. package/src/styles/theme.css +213 -0
  237. package/src/styles/theme.test.ts +196 -0
  238. package/src/utils/clipboardPayload.test.ts +42 -2
  239. package/src/utils/clipboardPayload.ts +57 -32
  240. package/src/utils/previewSafeMargins.test.ts +34 -0
  241. package/src/utils/previewSafeMargins.ts +10 -0
  242. package/src/utils/razorSplitTransaction.test.ts +0 -1
  243. package/src/utils/sdkCanvasComposition.test.ts +77 -0
  244. package/src/utils/studioUiPreferences.test.ts +43 -0
  245. package/src/utils/studioUiPreferences.ts +34 -6
  246. package/src/utils/timelineDropTrackInsert.test.ts +134 -0
  247. package/src/utils/timelineDropTrackInsert.ts +83 -0
  248. package/src/utils/timelineElementSplit.test.ts +1 -2
  249. package/src/utils/timelineElementSplit.ts +0 -1
  250. package/src/webmcp/tools/lookTools.test.ts +40 -0
  251. package/src/webmcp/tools/lookTools.ts +32 -1
  252. package/dist/assets/index-B9LfWNyM.css +0 -1
  253. package/dist/assets/index-Bgmx2h7W.js +0 -441
  254. package/src/components/StudioGlobalDragOverlay.tsx +0 -24
  255. package/src/components/storyboard/AgentChatMessageButton.test.tsx +0 -47
  256. package/src/components/storyboard/AgentChatMessageButton.tsx +0 -45
  257. package/src/components/storyboard/FramePoster.test.tsx +0 -61
  258. package/src/components/storyboard/FramePoster.tsx +0 -86
  259. package/src/components/storyboard/StoryboardDirection.tsx +0 -45
  260. package/src/components/storyboard/StoryboardFrameFocus.tsx +0 -412
  261. package/src/components/storyboard/StoryboardFrameTile.tsx +0 -117
  262. package/src/components/storyboard/StoryboardGrid.tsx +0 -55
  263. package/src/components/storyboard/StoryboardLoaded.tsx +0 -307
  264. package/src/components/storyboard/StoryboardReviewGuide.tsx +0 -300
  265. package/src/components/storyboard/StoryboardScriptPanel.tsx +0 -26
  266. package/src/components/storyboard/StoryboardSourceEditor.tsx +0 -231
  267. package/src/components/storyboard/StoryboardStatusLegend.tsx +0 -21
  268. package/src/components/storyboard/StoryboardView.tsx +0 -169
  269. package/src/components/storyboard/StoryboardViewModeGuard.test.tsx +0 -187
  270. package/src/components/storyboard/frameComments.test.ts +0 -95
  271. package/src/components/storyboard/frameComments.ts +0 -115
  272. package/src/components/storyboard/frameStatus.ts +0 -36
  273. package/src/components/storyboard/storyboardReviewStage.test.ts +0 -40
  274. package/src/components/storyboard/storyboardReviewStage.ts +0 -45
  275. package/src/components/storyboard/useFrameComments.ts +0 -98
  276. package/src/contexts/ViewModeContext.tsx +0 -142
  277. package/src/hooks/useProjectSignaturePoll.test.tsx +0 -135
  278. package/src/hooks/useProjectSignaturePoll.ts +0 -68
  279. package/src/hooks/useStoryboard.ts +0 -99
  280. package/src/player/hooks/useExpandedTimelineElements.test.ts +0 -774
  281. package/src/player/hooks/useExpandedTimelineElements.ts +0 -460
@@ -4,8 +4,14 @@ import {
4
4
  parseTimelineCompositionPayload,
5
5
  TIMELINE_COMPOSITION_MIME,
6
6
  } from "../../utils/timelineCompositionDrop";
7
- import { resolveTimelineAssetDrop, type TimelineRowGeometry } from "./timelineLayout";
8
- import type { TimelineDropCallbacks } from "./timelineCallbacks";
7
+ import {
8
+ getTimelineInsertBoundaryBand,
9
+ getTimelineRowPositionFromY,
10
+ resolveTimelineAssetDrop,
11
+ type TimelineRowGeometry,
12
+ } from "./timelineLayout";
13
+ import { resolveInsertRow } from "./timelineCollision";
14
+ import type { TimelineDropCallbacks, TimelineDropPlacement } from "./timelineCallbacks";
9
15
  import {
10
16
  applyTimelineAutoScrollStep,
11
17
  resolveTimelineAutoScrollLoopAction,
@@ -20,8 +26,6 @@ interface UseTimelineAssetDropOptions extends TimelineDropCallbacks {
20
26
  sessionEpoch: number;
21
27
  }
22
28
 
23
- type TimelinePlacement = { start: number; track: number };
24
-
25
29
  /**
26
30
  * Parse a JSON drag payload and, if it yields a value, forward it to the drop
27
31
  * callback. Malformed payloads are ignored. Shared by the asset + block paths so
@@ -30,8 +34,8 @@ type TimelinePlacement = { start: number; track: number };
30
34
  function applyJsonDropPayload(
31
35
  raw: string,
32
36
  pick: (parsed: Record<string, string | undefined>) => string | undefined,
33
- apply: (value: string, placement: TimelinePlacement) => void,
34
- placement: TimelinePlacement,
37
+ apply: (value: string, placement: TimelineDropPlacement) => void,
38
+ placement: TimelineDropPlacement,
35
39
  ): boolean {
36
40
  try {
37
41
  const value = pick(JSON.parse(raw) as Record<string, string | undefined>);
@@ -43,6 +47,40 @@ function applyJsonDropPayload(
43
47
  }
44
48
  }
45
49
 
50
+ /** Only file and asset drops are written by the asset op that can open a track. */
51
+ function canInsertTrackFor(types: readonly string[]): boolean {
52
+ return types.includes("Files") || types.includes(TIMELINE_ASSET_MIME);
53
+ }
54
+
55
+ /** The row boundary a pointer at `contentY` arms for a new track, or null over a lane's middle. */
56
+ export function resolveDropInsertRow(
57
+ contentY: number,
58
+ rowHeights: readonly number[] | undefined,
59
+ trackCount: number,
60
+ ): number | null {
61
+ if (trackCount === 0) return null;
62
+ const { rowFloat, rowHeight } = getTimelineRowPositionFromY(contentY, rowHeights);
63
+ // Past the last lane the drop already appends a track (getDefaultDroppedTrack).
64
+ if (rowFloat >= trackCount) return null;
65
+ return resolveInsertRow(rowFloat, trackCount, getTimelineInsertBoundaryBand(rowHeight));
66
+ }
67
+
68
+ function placeDrop(
69
+ geometry: Parameters<typeof resolveTimelineAssetDrop>[0],
70
+ clientX: number,
71
+ clientY: number,
72
+ // Blocks and compositions are written by their own installers, which only take a track.
73
+ canInsertTrack: boolean,
74
+ ): TimelineDropPlacement {
75
+ const placement = resolveTimelineAssetDrop(geometry, clientX, clientY);
76
+ if (!canInsertTrack) return placement;
77
+ const contentY = clientY - geometry.rectTop + geometry.scrollTop;
78
+ const insertRow = resolveDropInsertRow(contentY, geometry.rowHeights, geometry.trackOrder.length);
79
+ return insertRow == null
80
+ ? placement
81
+ : { ...placement, insertRow, trackOrder: geometry.trackOrder };
82
+ }
83
+
46
84
  function invokeDropCallback(callback: () => Promise<void> | void): void {
47
85
  try {
48
86
  void Promise.resolve(callback()).catch(() => undefined);
@@ -54,7 +92,7 @@ function invokeDropCallback(callback: () => Promise<void> | void): void {
54
92
  function applyFileDrop(
55
93
  transfer: DataTransfer,
56
94
  onFileDrop: TimelineDropCallbacks["onFileDrop"],
57
- placement: TimelinePlacement,
95
+ placement: TimelineDropPlacement,
58
96
  ): boolean {
59
97
  if (!onFileDrop || transfer.files.length === 0) return false;
60
98
  invokeDropCallback(() => onFileDrop(Array.from(transfer.files), placement));
@@ -65,8 +103,8 @@ function applyTypedJsonDrop(
65
103
  transfer: DataTransfer,
66
104
  mime: string,
67
105
  field: "name" | "path",
68
- apply: ((value: string, placement: TimelinePlacement) => Promise<void> | void) | undefined,
69
- placement: TimelinePlacement,
106
+ apply: ((value: string, placement: TimelineDropPlacement) => Promise<void> | void) | undefined,
107
+ placement: TimelineDropPlacement,
70
108
  ): boolean {
71
109
  if (!apply || !Array.from(transfer.types).includes(mime)) return false;
72
110
  const payload = transfer.getData(mime);
@@ -98,6 +136,7 @@ export function useTimelineAssetDrop({
98
136
  sessionEpoch,
99
137
  }: UseTimelineAssetDropOptions) {
100
138
  const [isDragOver, setIsDragOver] = useState(false);
139
+ const [dropPreview, setDropPreview] = useState<TimelineDropPlacement | null>(null);
101
140
  const dragPointerRef = useRef<{ clientX: number; clientY: number; sessionEpoch: number } | null>(
102
141
  null,
103
142
  );
@@ -142,6 +181,29 @@ export function useTimelineAssetDrop({
142
181
  [scrollRef, sessionEpoch, stepAutoScroll],
143
182
  );
144
183
 
184
+ const resolveDropPlacement = useCallback(
185
+ (clientX: number, clientY: number, canInsertTrack: boolean): TimelineDropPlacement => {
186
+ const scroll = scrollRef.current;
187
+ const rect = scroll?.getBoundingClientRect();
188
+ return placeDrop(
189
+ {
190
+ rectLeft: rect?.left ?? 0,
191
+ rectTop: rect?.top ?? 0,
192
+ scrollLeft: scroll?.scrollLeft ?? 0,
193
+ scrollTop: scroll?.scrollTop ?? 0,
194
+ contentOrigin,
195
+ pixelsPerSecond: ppsRef.current,
196
+ rowHeights: rowGeometryRef.current.rowHeights,
197
+ trackOrder: trackOrderRef.current,
198
+ },
199
+ clientX,
200
+ clientY,
201
+ canInsertTrack,
202
+ );
203
+ },
204
+ [scrollRef, ppsRef, trackOrderRef, rowGeometryRef, contentOrigin],
205
+ );
206
+
145
207
  const handleAssetDragOver = useCallback(
146
208
  (e: React.DragEvent) => {
147
209
  const types = Array.from(e.dataTransfer.types);
@@ -154,15 +216,22 @@ export function useTimelineAssetDrop({
154
216
  e.dataTransfer.dropEffect = "copy";
155
217
  activeDropEpochRef.current = sessionEpoch;
156
218
  setIsDragOver(true);
219
+ const next = resolveDropPlacement(e.clientX, e.clientY, canInsertTrackFor(types));
220
+ setDropPreview((prev) =>
221
+ prev?.start === next.start && prev.track === next.track && prev.insertRow === next.insertRow
222
+ ? prev
223
+ : next,
224
+ );
157
225
  syncAutoScroll(e.clientX, e.clientY);
158
226
  },
159
- [sessionEpoch, syncAutoScroll],
227
+ [resolveDropPlacement, sessionEpoch, syncAutoScroll],
160
228
  );
161
229
 
162
230
  const clearDropPreview = useCallback(() => {
163
231
  activeDropEpochRef.current = null;
164
232
  stopAutoScroll();
165
233
  setIsDragOver(false);
234
+ setDropPreview(null);
166
235
  }, [stopAutoScroll]);
167
236
 
168
237
  const handleAssetDragLeave = useCallback(
@@ -174,35 +243,14 @@ export function useTimelineAssetDrop({
174
243
  [clearDropPreview],
175
244
  );
176
245
 
177
- const resolveDropPlacement = useCallback(
178
- (clientX: number, clientY: number): TimelinePlacement => {
179
- const scroll = scrollRef.current;
180
- const rect = scroll?.getBoundingClientRect();
181
- return resolveTimelineAssetDrop(
182
- {
183
- rectLeft: rect?.left ?? 0,
184
- rectTop: rect?.top ?? 0,
185
- scrollLeft: scroll?.scrollLeft ?? 0,
186
- scrollTop: scroll?.scrollTop ?? 0,
187
- contentOrigin,
188
- pixelsPerSecond: ppsRef.current,
189
- rowHeights: rowGeometryRef.current.rowHeights,
190
- trackOrder: trackOrderRef.current,
191
- },
192
- clientX,
193
- clientY,
194
- );
195
- },
196
- [scrollRef, ppsRef, trackOrderRef, rowGeometryRef, contentOrigin],
197
- );
198
-
199
246
  const handleAssetDrop = useCallback(
200
247
  (e: React.DragEvent) => {
201
248
  e.preventDefault();
202
249
  const canCommit = activeDropEpochRef.current === sessionEpoch;
203
250
  clearDropPreview();
204
251
  if (!canCommit) return;
205
- const placement = resolveDropPlacement(e.clientX, e.clientY);
252
+ const types = Array.from(e.dataTransfer.types);
253
+ const placement = resolveDropPlacement(e.clientX, e.clientY, canInsertTrackFor(types));
206
254
 
207
255
  const compositionPayload = parseTimelineCompositionPayload(
208
256
  e.dataTransfer.getData(TIMELINE_COMPOSITION_MIME),
@@ -240,6 +288,7 @@ export function useTimelineAssetDrop({
240
288
 
241
289
  return {
242
290
  isDragOver,
291
+ dropPreview,
243
292
  handleAssetDragOver,
244
293
  handleAssetDragLeave,
245
294
  handleAssetDrop,
@@ -42,10 +42,9 @@ export function getTimelineEditCapabilities(input: {
42
42
  playbackStart?: number;
43
43
  playbackStartAttr?: "media-start" | "playback-start";
44
44
  sourceDuration?: number;
45
- timingSource?: "authored" | "implicit";
46
45
  timelineLocked?: boolean;
47
46
  }): TimelineEditCapabilities {
48
- if (input.timingSource === "implicit" || input.timelineLocked) {
47
+ if (input.timelineLocked) {
49
48
  return { canMove: false, canTrimStart: false, canTrimEnd: false };
50
49
  }
51
50
 
@@ -412,21 +412,6 @@ describe("getTimelineEditCapabilities", () => {
412
412
  });
413
413
  });
414
414
 
415
- it("keeps implicit layout layers selectable but not timeline-editable", () => {
416
- expect(
417
- getTimelineEditCapabilities({
418
- duration: 8,
419
- selector: ".scene-shell",
420
- tag: "div",
421
- timingSource: "implicit",
422
- }),
423
- ).toEqual({
424
- canMove: false,
425
- canTrimStart: false,
426
- canTrimEnd: false,
427
- });
428
- });
429
-
430
415
  it("allows move and both trims for patchable media clips with offset support", () => {
431
416
  expect(
432
417
  getTimelineEditCapabilities({
@@ -496,7 +481,6 @@ describe("getTimelineEditCapabilities", () => {
496
481
  tag: "div",
497
482
  duration: 4,
498
483
  selector: "#hero-card",
499
- timingSource: "authored",
500
484
  }),
501
485
  ).toEqual({
502
486
  canMove: true,
@@ -34,11 +34,9 @@ describe("buildTimelineGroupResizeMembers (legacy 36413da7f semantics)", () => {
34
34
  expect(buildTimelineGroupResizeMembers([a, locked], keys("a", "b"), "a", "end")).toBeNull();
35
35
  });
36
36
 
37
- it("degrades when a member is implicitly timed or has no patch target", () => {
37
+ it("degrades when a member has no patch target", () => {
38
38
  const a = el("a");
39
- const implicit = el("b", { timingSource: "implicit" });
40
39
  const noTarget = el("c", { domId: undefined, selector: undefined });
41
- expect(buildTimelineGroupResizeMembers([a, implicit], keys("a", "b"), "a", "end")).toBeNull();
42
40
  expect(buildTimelineGroupResizeMembers([a, noTarget], keys("a", "c"), "a", "end")).toBeNull();
43
41
  });
44
42
 
@@ -198,7 +198,6 @@ function canTrimEdge(element: TimelineElement, edge: TimelineGroupResizeEdge): b
198
198
  playbackStart: element.playbackStart,
199
199
  playbackStartAttr: element.playbackStartAttr,
200
200
  sourceDuration: element.sourceDuration,
201
- timingSource: element.timingSource,
202
201
  timelineLocked: element.timelineLocked,
203
202
  });
204
203
  return edge === "start" ? caps.canTrimStart : caps.canTrimEnd;
@@ -8,6 +8,7 @@ import type { TrackVisualStyle } from "./timelineIcons";
8
8
  import type { DraggedClipState, ResizingClipState, BlockedClipState } from "./useTimelineClipDrag";
9
9
  import type { TimelineClipIndex, TimelineTimeRange } from "../lib/timelineClipIndex";
10
10
  import type { TimelineRowGeometry } from "./timelineLayout";
11
+ import type { TimelineSnapTarget } from "./timelineSnapping";
11
12
  import type { TimelineVirtualRow } from "./useTimelineVirtualRows";
12
13
  import type { MultiDragPreviewInput } from "./timelineMultiDragPreview";
13
14
  import type { TimelineEditCallbacks } from "./timelineCallbacks";
@@ -112,6 +113,8 @@ export interface TimelineLaneBaseProps {
112
113
  export interface TimelineLanesProps extends TimelineLaneBaseProps {
113
114
  /** Live-derived by TimelineCanvas from {@link TimelineLaneBaseProps.draggedClip}. */
114
115
  draggedElement: TimelineElement | null;
116
+ /** Live move or trim snap target, resolved once by TimelineCanvas. */
117
+ snapGuide: TimelineSnapTarget | null;
115
118
  multiDragPreview: MultiDragPreviewInput | null;
116
119
  onToggleTrackHidden: TimelineEditCallbacks["onToggleTrackHidden"];
117
120
  onTogglePropertyGroupKeyframe: TimelineEditCallbacks["onTogglePropertyGroupKeyframe"];
@@ -4,6 +4,7 @@ import {
4
4
  collectTimelineSnapTargets,
5
5
  snapMoveToTargets,
6
6
  snapTimelineTime,
7
+ resolveSnapGuide,
7
8
  } from "./timelineSnapping";
8
9
 
9
10
  describe("collectTimelineSnapTargets", () => {
@@ -26,6 +27,18 @@ describe("collectTimelineSnapTargets", () => {
26
27
  expect(targets).toContainEqual({ time: 0.5, type: "beat" });
27
28
  });
28
29
 
30
+ it("omits the playhead when includePlayhead is false, for a trim", () => {
31
+ const targets = collectTimelineSnapTargets({
32
+ elements,
33
+ playheadTime: 7.25,
34
+ beatTimes: [0.5],
35
+ includePlayhead: false,
36
+ });
37
+ expect(targets.some((t) => t.type === "playhead")).toBe(false);
38
+ expect(targets).toContainEqual({ time: 2, type: "clip-edge" });
39
+ expect(targets).toContainEqual({ time: 0.5, type: "beat" });
40
+ });
41
+
29
42
  it("excludes the dragged element's own edges", () => {
30
43
  const targets = collectTimelineSnapTargets({
31
44
  elements,
@@ -132,3 +145,17 @@ describe("snapMoveToTargets", () => {
132
145
  expect(r.snapTime).toBeNull();
133
146
  });
134
147
  });
148
+
149
+ describe("resolveSnapGuide", () => {
150
+ it("prefers a started move, falls back to a trim, and is null when neither snapped", () => {
151
+ const move = { started: true, snapTime: 2, snapType: "playhead" as const };
152
+ const trim = { snapTime: 5, snapType: "clip-edge" as const };
153
+ expect(resolveSnapGuide(move, trim)).toEqual({ time: 2, type: "playhead" });
154
+ expect(resolveSnapGuide({ ...move, started: false }, trim)).toEqual({
155
+ time: 5,
156
+ type: "clip-edge",
157
+ });
158
+ expect(resolveSnapGuide(null, { snapTime: null, snapType: null })).toBeNull();
159
+ expect(resolveSnapGuide(null, null)).toBeNull();
160
+ });
161
+ });
@@ -7,6 +7,16 @@ export interface TimelineSnapTarget {
7
7
  type: TimelineSnapType;
8
8
  }
9
9
 
10
+ /** The guide the canvas draws: the live move's snap target, else the live trim's. */
11
+ export function resolveSnapGuide(
12
+ moving: { started: boolean; snapTime: number | null; snapType: TimelineSnapType | null } | null,
13
+ trimming: { snapTime?: number | null; snapType?: TimelineSnapType | null } | null,
14
+ ): TimelineSnapTarget | null {
15
+ const source = moving?.started ? moving : trimming;
16
+ if (source?.snapTime == null || source.snapType == null) return null;
17
+ return { time: source.snapTime, type: source.snapType };
18
+ }
19
+
10
20
  /** Pixel radius within which a time snaps to a target (matches historical beat snap). */
11
21
  export const TIMELINE_SNAP_PX = 8;
12
22
 
@@ -21,6 +31,8 @@ export function collectTimelineSnapTargets(input: {
21
31
  playheadTime: number | null;
22
32
  beatTimes: readonly number[];
23
33
  excludeElementKey?: string | null;
34
+ /** A trim excludes the playhead: the dragged edge drives it, so snapping to it is circular. */
35
+ includePlayhead?: boolean;
24
36
  }): TimelineSnapTarget[] {
25
37
  const byTime = new Map<number, TimelineSnapTarget>();
26
38
  const add = (time: number, type: TimelineSnapType) => {
@@ -38,7 +50,8 @@ export function collectTimelineSnapTargets(input: {
38
50
  add(el.start, "clip-edge");
39
51
  add(el.start + el.duration, "clip-edge");
40
52
  }
41
- if (input.playheadTime != null) add(input.playheadTime, "playhead");
53
+ if (input.playheadTime != null && input.includePlayhead !== false)
54
+ add(input.playheadTime, "playhead");
42
55
 
43
56
  return Array.from(byTime.values()).sort((a, b) => a.time - b.time);
44
57
  }
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * `TimelineElement.track` is a z-order SORT key, not a row number: an expanded
5
5
  * sub-composition child gets a synthesized fractional key (`host.track + n /
6
- * (siblings + 2)`, see `useExpandedTimelineElements`), so putting it in a string
6
+ * (siblings + 2)`, see `useTimelineRowElements`), so putting it in a string
7
7
  * announces "Hide track 0.16666666666666666". Every user-visible track number,
8
8
  * whether it is rendered by a component or baked into an undo-history label,
9
9
  * routes through here; the raw key stays in callbacks and lookups only.
@@ -2,7 +2,6 @@ import { describe, expect, it, vi } from "vitest";
2
2
  import type { TimelineElement } from "../store/playerStore";
3
3
  import type { ClipManifestClip } from "../lib/playbackTypes";
4
4
  import { createTimelineElementFromManifestClip } from "../lib/timelineDOM";
5
- import { buildExpandedElements } from "../hooks/useExpandedTimelineElements";
6
5
  import { normalizeToZones } from "./timelineZones";
7
6
  import { commitDraggedClipMove, type TimelineMoveEdit } from "./timelineClipDragCommit";
8
7
  import type { DraggedClipState } from "./useTimelineClipDrag";
@@ -113,56 +112,4 @@ describe("track persist pipeline (manifest → factory → lanes → drag commit
113
112
  const up = commitLaneChange(byId.get("g")!, 0, elements, [0, 1, 2]);
114
113
  expect(up[0].updates.track).toBe(3); // authored, NOT display lane 0
115
114
  });
116
-
117
- it("an expanded sub-comp child's lane change persists the sibling's authored track from ITS file", () => {
118
- // Host timeline: the sub-comp host plus a root clip, discovered through the
119
- // factory and lane-normalized like the store does.
120
- const hostManifest = [
121
- manifestClip({
122
- id: "scene",
123
- kind: "composition",
124
- tagName: "div",
125
- track: 0,
126
- start: 0,
127
- duration: 10,
128
- compositionId: "scene",
129
- compositionSrc: "scene.html",
130
- }),
131
- manifestClip({ id: "root-clip", kind: "video", tagName: "video", track: 1, start: 0 }),
132
- ];
133
- // scene.html has SPARSE authored tracks 3 and 7.
134
- const childClips = [
135
- manifestClip({ id: "c3", track: 3, start: 1, duration: 2, parentCompositionId: "scene" }),
136
- manifestClip({ id: "c7", track: 7, start: 4, duration: 2, parentCompositionId: "scene" }),
137
- ];
138
- const storeElements = normalizeToZones(fromManifest(hostManifest));
139
- const parentMap = new Map([
140
- ["c3", "scene"],
141
- ["c7", "scene"],
142
- ]);
143
- const expanded = buildExpandedElements(
144
- storeElements,
145
- [...hostManifest, ...childClips],
146
- parentMap,
147
- "scene",
148
- "scene",
149
- );
150
-
151
- // The children replaced the host row: synthetic display lanes, but the
152
- // authored track (in scene.html's coordinate space) survived the expansion.
153
- const c3 = expanded.find((e) => e.domId === "c3")!;
154
- const c7 = expanded.find((e) => e.domId === "c7")!;
155
- expect(c3).toMatchObject({ authoredTrack: 3, sourceFile: "scene.html" });
156
- expect(c7).toMatchObject({ authoredTrack: 7, sourceFile: "scene.html" });
157
- expect(c3.stackingContextId).toBe("root");
158
- expect(c3.track).not.toBe(3); // display row is synthetic
159
-
160
- // Drag c3 onto c7's display lane: the persist target is c3's OWN file, so
161
- // the written track must be c7's authored 7 — not the display-lane integer.
162
- const trackOrder = [...new Set(expanded.map((e) => e.track))].sort((a, b) => a - b);
163
- const edits = commitLaneChange(c3, c7.track, expanded, trackOrder);
164
- expect(edits).toHaveLength(1);
165
- expect(edits[0].updates.track).toBe(7);
166
- expect(edits[0].updates.track).not.toBe(c7.track);
167
- });
168
115
  });
@@ -39,7 +39,7 @@ function PropertyGroupNavigation({
39
39
  // bigger box; focus-visible matches every other control in this header.
40
40
  const CHEVRON_BUTTON_CLASS =
41
41
  "relative h-5 w-3 border-0 bg-transparent p-0 text-white/55 hover:text-white disabled:text-white/15 " +
42
- "focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC] " +
42
+ "focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-[#3CE6AC] " +
43
43
  "before:absolute before:left-1/2 before:top-1/2 before:h-6 before:w-6 " +
44
44
  "before:-translate-x-1/2 before:-translate-y-1/2 before:content-['']";
45
45
  const seekTo = (keyframe: { percentage: number } | null) => {
@@ -154,7 +154,7 @@ export function PropertyGroupHeaderRow({
154
154
  aria-label={`${navigation.currentKeyframe ? "Remove" : "Add"} ${label} keyframe`}
155
155
  title={`${navigation.currentKeyframe ? "Remove" : "Add"} ${label} keyframe`}
156
156
  // h-6 w-6 = the 24x24 WCAG 2.2 minimum target; the ◆ glyph stays 11px.
157
- className="flex h-6 w-6 shrink-0 items-center justify-center border-0 bg-transparent p-0 text-[11px] text-[#3CE6AC] focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC]"
157
+ className="flex h-6 w-6 shrink-0 items-center justify-center border-0 bg-transparent p-0 text-[11px] text-[#3CE6AC] focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-[#3CE6AC]"
158
158
  onClick={(event) => {
159
159
  // Same as the disclosure caret and the eye: a control in the label
160
160
  // column owns its click, it does not also hit the track row behind it.
@@ -262,7 +262,7 @@ export function AutomationLaneHeaderRow({
262
262
  aria-label={onReveal ? `Show ${label} in the effect rack` : undefined}
263
263
  title={onReveal ? `Show ${label} in the effect rack` : label}
264
264
  disabled={!onReveal}
265
- className="flex min-w-0 flex-1 flex-col justify-center rounded border-0 bg-transparent p-0 text-left leading-tight enabled:hover:text-white focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC]"
265
+ className="flex min-w-0 flex-1 flex-col justify-center rounded-sm border-0 bg-transparent p-0 text-left leading-tight enabled:hover:text-white focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-[#3CE6AC]"
266
266
  onPointerDown={(event) => event.stopPropagation()}
267
267
  onClick={(event) => {
268
268
  // The label column owns its click; it does not also fall through to
@@ -311,7 +311,7 @@ export function AutomationLaneHeaderRow({
311
311
  aria-label={`Remove ${label} automation`}
312
312
  title={`Remove ${label} automation`}
313
313
  // h-6 w-6 is the 24x24 WCAG 2.2 target; the glyph stays small.
314
- className="flex h-6 w-6 shrink-0 items-center justify-center border-0 bg-transparent p-0 text-[11px] text-white/35 hover:text-white focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC]"
314
+ className="flex h-6 w-6 shrink-0 items-center justify-center border-0 bg-transparent p-0 text-[11px] text-white/35 hover:text-white focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-[#3CE6AC]"
315
315
  onPointerDown={(event) => event.stopPropagation()}
316
316
  onClick={(event) => {
317
317
  // A control in the label column owns its click; it does not also hit
@@ -0,0 +1,32 @@
1
+ import { useCallback } from "react";
2
+ import { usePlayerStore, type TimelineElement } from "../store/playerStore";
3
+ import type { ClipContextMenuState } from "./TimelineOverlays";
4
+
5
+ /** A clicked member of an active multi-selection keeps the whole group
6
+ * selected, so the context menu it opens acts on all of it. */
7
+ export function useClipContextMenu(
8
+ onSelectElement: ((element: TimelineElement | null) => void) | undefined,
9
+ dismissGapMenu: () => void,
10
+ setClipContextMenu: (state: ClipContextMenuState | null) => void,
11
+ ) {
12
+ const selectedElementIds = usePlayerStore((s) => s.selectedElementIds);
13
+ const setSelectedElementId = usePlayerStore((s) => s.setSelectedElementId);
14
+ return useCallback(
15
+ (e: React.MouseEvent, el: TimelineElement) => {
16
+ e.preventDefault();
17
+ const id = el.key ?? el.id;
18
+ if (!(selectedElementIds.size > 1 && selectedElementIds.has(id))) {
19
+ setSelectedElementId(id);
20
+ onSelectElement?.(el);
21
+ }
22
+ dismissGapMenu();
23
+ setClipContextMenu({
24
+ x: e.clientX,
25
+ y: e.clientY,
26
+ element: el,
27
+ sessionEpoch: usePlayerStore.getState().timelineSessionEpoch,
28
+ });
29
+ },
30
+ [selectedElementIds, setSelectedElementId, onSelectElement, dismissGapMenu, setClipContextMenu],
31
+ );
32
+ }