@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
@@ -37,7 +37,7 @@ export function LaneToggleButton({
37
37
  title={`${isExpanded ? "Hide" : "Show"} lanes`}
38
38
  // h-6 w-6 = the 24x24 WCAG 2.2 minimum target. The glyph stays 11px;
39
39
  // only the hit box grows.
40
- className={`ml-auto flex h-6 w-6 shrink-0 items-center justify-center rounded border-0 bg-transparent p-0 text-[11px] leading-none focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC] ${
40
+ className={`ml-auto flex h-6 w-6 shrink-0 items-center justify-center rounded border-0 bg-transparent p-0 text-[11px] leading-none focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-[#3CE6AC] ${
41
41
  isExpanded ? "text-[#3CE6AC]" : "text-white/55 hover:text-white"
42
42
  }`}
43
43
  onPointerDown={(event) => event.stopPropagation()}
@@ -97,7 +97,7 @@ export function LayerDisclosureRow({
97
97
  </span>
98
98
  {/* Wraps rather than truncating: a truncated name needs a hover to be
99
99
  read, which a scanned column cannot rely on. */}
100
- <span className="min-w-0 flex-1 break-words font-medium leading-tight">{name}</span>
100
+ <span className="min-w-0 flex-1 wrap-break-word font-medium leading-tight">{name}</span>
101
101
  <TrackClipCount clipCount={clipCount} />
102
102
  {children}
103
103
  {/* Anchored right, on every header that has one: the lane toggle is the
@@ -116,7 +116,7 @@ const FullscreenButton = memo(function FullscreenButton({
116
116
  trackStudioEvent("playback", { action: "fullscreen_toggle", active: !isFullscreen });
117
117
  onToggleFullscreen();
118
118
  }}
119
- className={`flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-md transition-colors ${
119
+ className={`flex h-7 w-7 shrink-0 items-center justify-center rounded-md transition-colors ${
120
120
  isFullscreen ? "text-studio-accent" : "text-neutral-500 hover:text-neutral-200"
121
121
  }`}
122
122
  aria-label={isFullscreen ? "Exit fullscreen" : "Enter fullscreen"}
@@ -153,7 +153,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
153
153
  );
154
154
 
155
155
  return (
156
- <div ref={shortcutsPanelRef} className="relative flex-shrink-0">
156
+ <div ref={shortcutsPanelRef} className="relative shrink-0">
157
157
  <Tooltip label="Shortcuts and tools">
158
158
  <button
159
159
  ref={triggerRef}
@@ -192,7 +192,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
192
192
  // Deliberately NOT aria-modal. This is a non-modal disclosure: focus is
193
193
  // not trapped and the rest of the editor stays operable, so claiming
194
194
  // modality would make assistive tech treat the whole app as inert.
195
- className="absolute bottom-full right-0 mb-2 z-50 rounded-lg shadow-xl min-w-[220px] overflow-y-auto outline-none"
195
+ className="absolute bottom-full right-0 mb-2 z-50 rounded-lg shadow-xl min-w-[220px] overflow-y-auto outline-hidden"
196
196
  style={{
197
197
  background: "#161618",
198
198
  border: "1px solid rgba(255,255,255,0.08)",
@@ -212,7 +212,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
212
212
  pattern="[0-9]*"
213
213
  aria-label="Jump to frame"
214
214
  placeholder="frame number"
215
- className="h-6 flex-1 rounded border border-neutral-700 bg-neutral-900 px-2 text-[10px] font-mono tabular-nums text-neutral-200 outline-none transition-colors placeholder:text-neutral-600 focus:border-studio-accent/60"
215
+ className="h-6 flex-1 rounded-sm border border-neutral-700 bg-neutral-900 px-2 text-[10px] font-mono tabular-nums text-neutral-200 outline-hidden transition-colors placeholder:text-neutral-600 focus:border-studio-accent/60"
216
216
  onKeyDown={handleJumpKeyDown}
217
217
  onBlur={commitJumpFrame}
218
218
  />
@@ -220,7 +220,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
220
220
  <button
221
221
  type="submit"
222
222
  disabled={disabled}
223
- className="h-6 px-2 rounded border border-neutral-700 text-[10px] text-neutral-300 transition-colors hover:border-neutral-500 hover:bg-neutral-800 disabled:opacity-40"
223
+ className="h-6 px-2 rounded-sm border border-neutral-700 text-[10px] text-neutral-300 transition-colors hover:border-neutral-500 hover:bg-neutral-800 disabled:opacity-40"
224
224
  >
225
225
  Go
226
226
  </button>
@@ -236,7 +236,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
236
236
  <div className="flex items-center justify-between gap-2">
237
237
  <div className="flex items-center gap-2">
238
238
  <span
239
- className="font-mono text-[10px] rounded border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[20px] text-center"
239
+ className="font-mono text-[10px] rounded-sm border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[20px] text-center"
240
240
  style={{ background: "rgba(255,255,255,0.05)" }}
241
241
  >
242
242
  I
@@ -253,7 +253,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
253
253
  <button
254
254
  type="button"
255
255
  onClick={() => setInPoint(null)}
256
- className="w-4 h-4 flex items-center justify-center rounded text-neutral-500 hover:text-neutral-200 transition-colors"
256
+ className="w-4 h-4 flex items-center justify-center rounded-sm text-neutral-500 hover:text-neutral-200 transition-colors"
257
257
  aria-label="Clear in-point"
258
258
  >
259
259
  <svg
@@ -277,7 +277,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
277
277
  <div className="flex items-center justify-between gap-2">
278
278
  <div className="flex items-center gap-2">
279
279
  <span
280
- className="font-mono text-[10px] rounded border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[20px] text-center"
280
+ className="font-mono text-[10px] rounded-sm border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[20px] text-center"
281
281
  style={{ background: "rgba(255,255,255,0.05)" }}
282
282
  >
283
283
  O
@@ -294,7 +294,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
294
294
  <button
295
295
  type="button"
296
296
  onClick={() => setOutPoint(null)}
297
- className="w-4 h-4 flex items-center justify-center rounded text-neutral-500 hover:text-neutral-200 transition-colors"
297
+ className="w-4 h-4 flex items-center justify-center rounded-sm text-neutral-500 hover:text-neutral-200 transition-colors"
298
298
  aria-label="Clear out-point"
299
299
  >
300
300
  <svg
@@ -328,7 +328,7 @@ export const ShortcutsPanel = memo(function ShortcutsPanel({
328
328
  {section.hints.map((hint) => (
329
329
  <div key={hint.key} className="flex items-center gap-3">
330
330
  <span
331
- className="font-mono text-[10px] rounded border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[36px] text-center"
331
+ className="font-mono text-[10px] rounded-sm border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[36px] text-center"
332
332
  style={{ background: "rgba(255,255,255,0.05)" }}
333
333
  >
334
334
  {hint.key}
@@ -23,7 +23,7 @@ export const SpeedMenu = memo(function SpeedMenu({
23
23
  const speedMenuContainerRef = useContextMenuDismiss(closeMenu);
24
24
 
25
25
  return (
26
- <div ref={speedMenuContainerRef} className="relative flex-shrink-0">
26
+ <div ref={speedMenuContainerRef} className="relative shrink-0">
27
27
  <Tooltip label="Playback speed">
28
28
  <button
29
29
  type="button"
@@ -57,10 +57,8 @@ export const SpeedMenu = memo(function SpeedMenu({
57
57
  setPlaybackRate(rate);
58
58
  setShowSpeedMenu(false);
59
59
  }}
60
- className={`block w-full px-3 py-1.5 text-[11px] text-left font-mono tabular-nums transition-colors outline-none focus-visible:bg-white/[0.04] ${
61
- isCurrent
62
- ? "text-neutral-50 bg-white/[0.06]"
63
- : "text-neutral-500 hover:bg-white/[0.04]"
60
+ className={`block w-full px-3 py-1.5 text-[11px] text-left font-mono tabular-nums transition-colors outline-hidden focus-visible:bg-white/4 ${
61
+ isCurrent ? "text-neutral-50 bg-white/6" : "text-neutral-500 hover:bg-white/4"
64
62
  }`}
65
63
  >
66
64
  {rate}x
@@ -1,7 +1,7 @@
1
1
  import { useRef, useMemo, useCallback, useState, memo } from "react";
2
2
  import { useAdjustedBeatAnalysis, useMusicBeatAnalysis } from "../../hooks/useMusicBeatAnalysis";
3
3
  import { usePlayerStore, type TimelineElement } from "../store/playerStore";
4
- import { useExpandedTimelineElements } from "../hooks/useExpandedTimelineElements";
4
+ import { useTimelineRowElements } from "../hooks/useTimelineRowElements";
5
5
  import { defaultTimelineTheme } from "./timelineTheme";
6
6
  import { useTimelineRangeSelection } from "./useTimelineRangeSelection";
7
7
  import { useTimelinePlayhead } from "./useTimelinePlayhead";
@@ -43,6 +43,7 @@ import { useTimelineClipRenderWindow } from "./useTimelineClipRenderWindow";
43
43
  import { useTimelineActiveClips } from "./useTimelineActiveClips";
44
44
  import { useTimelineLaneMoveRefresh } from "./useTimelineLaneMoveRefresh";
45
45
  import { useTimelineLogicalFocus } from "./useTimelineLogicalFocus";
46
+ import { useClipContextMenu } from "./useTimelineClipContextMenu";
46
47
 
47
48
  export {
48
49
  shouldAutoScrollTimeline,
@@ -79,6 +80,10 @@ export const Timeline = memo(function Timeline({
79
80
  onBlockedEditAttempt: onBlockedEditAttemptOverride,
80
81
  onSplitElement: onSplitElementOverride,
81
82
  onSelectElement,
83
+ onCopyClip,
84
+ onPasteClip,
85
+ onDuplicateClip,
86
+ canPasteClip,
82
87
  theme: themeOverrides,
83
88
  sessionEpoch = 0,
84
89
  }: TimelineProps = {}) {
@@ -106,7 +111,7 @@ export const Timeline = memo(function Timeline({
106
111
  const refreshAfterLaneMove = useTimelineLaneMoveRefresh();
107
112
  useMusicBeatAnalysis();
108
113
  const rawElements = usePlayerStore((s) => s.elements);
109
- const expandedElements = useExpandedTimelineElements();
114
+ const expandedElements = useTimelineRowElements();
110
115
  const adjustedBeatAnalysis = useAdjustedBeatAnalysis();
111
116
  const duration = usePlayerStore((s) => s.duration);
112
117
  const timeDisplayMode = usePlayerStore((s) => s.timeDisplayMode);
@@ -208,6 +213,7 @@ export const Timeline = memo(function Timeline({
208
213
  onMoveElement: pinnedOnMoveElement,
209
214
  onMoveElements: pinnedOnMoveElements,
210
215
  });
216
+ const onContextMenuClip = useClipContextMenu(onSelectElement, dismissGapMenu, setClipContextMenu);
211
217
 
212
218
  const {
213
219
  draggedClip,
@@ -227,6 +233,7 @@ export const Timeline = memo(function Timeline({
227
233
  onResizeElement: pinnedOnResizeElement,
228
234
  onResizeElements: pinnedOnResizeElements,
229
235
  onBlockedEditAttempt,
236
+ onSeek,
230
237
  setShowPopover,
231
238
  setRangeSelectionRef,
232
239
  readZIndex: zSyncEnabled ? readClipZIndex : undefined,
@@ -458,7 +465,7 @@ export const Timeline = memo(function Timeline({
458
465
  data-timeline-scroll-viewport
459
466
  data-timeline-auto-scroll-left-inset={labelMode ? LABEL_COL_W : 0}
460
467
  tabIndex={-1}
461
- className={`${zoomMode === "fit" ? "overflow-x-hidden" : "overflow-x-auto"} overflow-y-auto h-full outline-none`}
468
+ className={`${zoomMode === "fit" ? "overflow-x-hidden" : "overflow-x-auto"} overflow-y-auto h-full outline-hidden`}
462
469
  onScroll={(e) => {
463
470
  lastScrollLeftRef.current = e.currentTarget.scrollLeft; // restored across post-edit reload
464
471
  recordTimelineScroll(e.currentTarget);
@@ -492,6 +499,7 @@ export const Timeline = memo(function Timeline({
492
499
  rangeSelection={rangeSelection}
493
500
  marqueeRect={marqueeRect}
494
501
  laneGapStrips={laneGapStrips}
502
+ dropPreview={assetDrop.dropPreview}
495
503
  theme={theme}
496
504
  displayTrackOrder={displayLayout.displayTrackOrder}
497
505
  rowHeights={displayLayout.displayRowHeights}
@@ -548,18 +556,7 @@ export const Timeline = memo(function Timeline({
548
556
  onShiftClickKeyframe={onShiftClickKeyframe}
549
557
  onMoveKeyframe={onMoveKeyframe}
550
558
  onContextMenuKeyframe={onContextMenuKeyframe}
551
- onContextMenuClip={(e, el) => {
552
- e.preventDefault();
553
- setSelectedElementId(el.key ?? el.id);
554
- onSelectElement?.(el);
555
- dismissGapMenu();
556
- setClipContextMenu({
557
- x: e.clientX,
558
- y: e.clientY,
559
- element: el,
560
- sessionEpoch: usePlayerStore.getState().timelineSessionEpoch,
561
- });
562
- }}
559
+ onContextMenuClip={onContextMenuClip}
563
560
  onContextMenuLane={(e, track, time) => {
564
561
  if (draggedClip?.started || resizingClip) return;
565
562
  setClipContextMenu(null);
@@ -588,6 +585,10 @@ export const Timeline = memo(function Timeline({
588
585
  onSplitElement={onSplitElement}
589
586
  pinZoomBeforeEdit={pinZoomBeforeEdit}
590
587
  onDeleteElement={_onDeleteElement}
588
+ onCopyClip={onCopyClip}
589
+ onPasteClip={onPasteClip}
590
+ onDuplicateClip={onDuplicateClip}
591
+ canPasteClip={canPasteClip}
591
592
  gapContextMenu={gapMenuModel}
592
593
  onDismissGapContextMenu={dismissGapMenu}
593
594
  onCloseTrackGap={closeTrackGap}
@@ -2,6 +2,7 @@ import { memo } from "react";
2
2
  import { TimelineRuler } from "./TimelineRuler";
3
3
  import { PlayheadIndicator } from "./PlayheadIndicator";
4
4
  import type { TimelineRangeSelection } from "./timelineEditing";
5
+ import type { TimelineDropPlacement } from "./timelineCallbacks";
5
6
  import {
6
7
  RULER_H,
7
8
  CLIP_Y,
@@ -23,6 +24,7 @@ import type { TimelineLaneBaseProps } from "./timelineLaneProps";
23
24
  import type { TimelineLaneGapStrips } from "./useTimelineGapHighlights";
24
25
  import { getTimelineElementIdentity } from "../lib/timelineElementHelpers";
25
26
  import { TimelineGestureOverlay } from "./TimelineGestureOverlay";
27
+ import { resolveSnapGuide } from "./timelineSnapping";
26
28
 
27
29
  interface TimelineCanvasProps extends TimelineLaneBaseProps {
28
30
  major: number[];
@@ -39,12 +41,25 @@ interface TimelineCanvasProps extends TimelineLaneBaseProps {
39
41
  playheadRef: React.RefObject<HTMLDivElement | null>;
40
42
  /** Gap strips: loud on gap-menu-row hover, quiet on the selected clip's lane. */
41
43
  laneGapStrips: TimelineLaneGapStrips[];
44
+ /** Landing spot of an outside drag in progress, or null. */
45
+ dropPreview: TimelineDropPlacement | null;
42
46
  }
43
47
 
48
+ // A dropped clip's length is unknown until it lands; the preview shows a default.
49
+ const DROP_PREVIEW_SECONDS = 3;
50
+
44
51
  export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvasProps) {
45
- const { draggedClip, scrollRef, selectedElementIds, displayTrackOrder } = props;
52
+ const { draggedClip, resizingClip, scrollRef, selectedElementIds, displayTrackOrder } = props;
53
+ const snapGuide = resolveSnapGuide(draggedClip, resizingClip);
46
54
  const draggedRowIndex =
47
55
  draggedClip?.started === true ? displayTrackOrder.indexOf(draggedClip.previewTrack) : -1;
56
+ const dropTrackIndex = props.dropPreview
57
+ ? displayTrackOrder.indexOf(props.dropPreview.track)
58
+ : -1;
59
+ // A track not in the order is a new track, drawn one row past the last lane.
60
+ const dropRowIndex = dropTrackIndex < 0 ? displayTrackOrder.length : dropTrackIndex;
61
+ const insertLineRow =
62
+ (draggedClip?.started ? draggedClip.insertRow : null) ?? props.dropPreview?.insertRow ?? null;
48
63
  const draggedRowHeight = getTimelineRowHeight(draggedRowIndex, props.rowHeights);
49
64
  // A clip bar in an EXPANDED row still renders at TRACK_H (the property lanes
50
65
  // occupy the rest of the row — see TimelineLanes' clipHeight), so the drag
@@ -107,6 +122,7 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
107
122
 
108
123
  <TimelineLanes
109
124
  {...props}
125
+ snapGuide={snapGuide}
110
126
  draggedElement={draggedElement}
111
127
  multiDragPreview={multiDragPreview}
112
128
  onToggleTrackHidden={onToggleTrackHidden}
@@ -171,13 +187,34 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
171
187
  />
172
188
  )}
173
189
 
190
+ {/* Drop preview: where an asset or file dragged in from outside will land
191
+ (a row past the last lane means a new track). */}
192
+ {props.dropPreview && props.dropPreview.insertRow == null && (
193
+ <div
194
+ aria-hidden="true"
195
+ data-testid="timeline-drop-preview"
196
+ className="absolute pointer-events-none"
197
+ style={{
198
+ top: getTimelineRowTop(dropRowIndex, props.rowHeights) + CLIP_Y,
199
+ left: props.contentOrigin + props.dropPreview.start * props.pps,
200
+ width: DROP_PREVIEW_SECONDS * props.pps,
201
+ height: TRACK_H - CLIP_Y * 2,
202
+ border: "1px solid rgba(60,230,172,0.55)",
203
+ background: "rgba(60,230,172,0.12)",
204
+ borderRadius: 4,
205
+ zIndex: 30,
206
+ }}
207
+ />
208
+ )}
209
+
174
210
  {/* Insertion line — a new track will be inserted at this boundary on drop.
175
211
  Shown while the pointer is near a lane boundary (insert mode). */}
176
- {draggedClip?.started && draggedClip.insertRow != null && (
212
+ {insertLineRow != null && (
177
213
  <div
214
+ data-testid="timeline-insert-line"
178
215
  className="absolute pointer-events-none"
179
216
  style={{
180
- top: getTimelineRowTop(draggedClip.insertRow, props.rowHeights) - 0.5,
217
+ top: getTimelineRowTop(insertLineRow, props.rowHeights) - 0.5,
181
218
  left: props.contentOrigin,
182
219
  width: props.trackContentWidth,
183
220
  height: 1,
@@ -188,18 +225,18 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
188
225
  />
189
226
  )}
190
227
 
191
- {/* Snap guide for non-beat targets during clip drag */}
192
- {draggedClip?.started && draggedClip.snapTime != null && draggedClip.snapType !== "beat" && (
228
+ {/* Snap guide for non-beat targets during a clip move or trim */}
229
+ {snapGuide && snapGuide.type !== "beat" && (
193
230
  <div
194
231
  className="absolute pointer-events-none"
195
232
  style={{
196
- left: props.contentOrigin + draggedClip.snapTime * props.pps,
233
+ left: props.contentOrigin + snapGuide.time * props.pps,
197
234
  top: RULER_H,
198
235
  bottom: 0,
199
236
  width: 1,
200
- background: draggedClip.snapType === "playhead" ? "#3CE6AC" : "rgba(255,255,255,0.6)",
237
+ background: snapGuide.type === "playhead" ? "#3CE6AC" : "rgba(255,255,255,0.6)",
201
238
  boxShadow:
202
- draggedClip.snapType === "playhead"
239
+ snapGuide.type === "playhead"
203
240
  ? "0 0 6px rgba(60,230,172,0.5)"
204
241
  : "0 0 6px rgba(255,255,255,0.4)",
205
242
  zIndex: 60,
@@ -203,7 +203,7 @@ function SegmentEaseControl({
203
203
  // for that overlay the button keeps its 16x16 hit area, which is WCAG's
204
204
  // target-spacing exception: the neighbouring diamonds are themselves the
205
205
  // reason it cannot grow, and stealing their clicks is the worse failure.
206
- className={`absolute flex items-center justify-center rounded ${restingVisible ? "opacity-40" : "opacity-0"} transition-opacity hover:opacity-100 group-hover:opacity-100 focus-visible:opacity-100 ${roomForFullTarget ? "before:absolute before:left-1/2 before:top-1/2 before:h-6 before:w-6 before:-translate-x-1/2 before:-translate-y-1/2 before:content-['']" : ""}`}
206
+ className={`absolute flex items-center justify-center rounded-sm ${restingVisible ? "opacity-40" : "opacity-0"} transition-opacity hover:opacity-100 group-hover:opacity-100 focus-visible:opacity-100 ${roomForFullTarget ? "before:absolute before:left-1/2 before:top-1/2 before:h-6 before:w-6 before:-translate-x-1/2 before:-translate-y-1/2 before:content-['']" : ""}`}
207
207
  style={{
208
208
  left: "50%",
209
209
  top: "50%",
@@ -19,7 +19,7 @@ export function TimelineEmptyState({
19
19
  return (
20
20
  <div
21
21
  className={`h-full border-t bg-[#0a0a0b] flex flex-col select-none transition-colors duration-150 ${
22
- isDragOver ? "border-studio-accent/50 bg-studio-accent/[0.03]" : "border-neutral-800/50"
22
+ isDragOver ? "border-studio-accent/50 bg-studio-accent/3" : "border-neutral-800/50"
23
23
  }`}
24
24
  onDragOver={onDragOver}
25
25
  onDragLeave={onDragLeave}
@@ -27,7 +27,7 @@ export function TimelineEmptyState({
27
27
  >
28
28
  {/* Ruler */}
29
29
  <div
30
- className="flex-shrink-0 border-b border-neutral-800/40 flex items-end relative"
30
+ className="shrink-0 border-b border-neutral-800/40 flex items-end relative"
31
31
  style={{ height: RULER_H, paddingLeft: GUTTER }}
32
32
  >
33
33
  {[0, 10, 20, 30, 40, 50].map((s) => (
@@ -49,7 +49,7 @@ export function TimelineEmptyState({
49
49
  <div className="flex-1 flex items-center justify-center">
50
50
  <div
51
51
  className={`flex items-center gap-3 px-6 py-3 border border-dashed rounded-lg transition-colors duration-150 ${
52
- isDragOver ? "border-studio-accent/60 bg-studio-accent/[0.06]" : "border-neutral-700/50"
52
+ isDragOver ? "border-studio-accent/60 bg-studio-accent/6" : "border-neutral-700/50"
53
53
  }`}
54
54
  >
55
55
  {isDragOver ? (
@@ -63,7 +63,7 @@ export function TimelineEmptyState({
63
63
  strokeWidth="1.5"
64
64
  strokeLinecap="round"
65
65
  strokeLinejoin="round"
66
- className="text-studio-accent flex-shrink-0"
66
+ className="text-studio-accent shrink-0"
67
67
  >
68
68
  <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
69
69
  <polyline points="7 10 12 15 17 10" />
@@ -82,7 +82,7 @@ export function TimelineEmptyState({
82
82
  strokeWidth="1.5"
83
83
  strokeLinecap="round"
84
84
  strokeLinejoin="round"
85
- className="text-neutral-600 flex-shrink-0"
85
+ className="text-neutral-600 shrink-0"
86
86
  >
87
87
  <rect x="2" y="2" width="20" height="20" rx="2" />
88
88
  <path d="M7 2v20" />
@@ -79,7 +79,7 @@ function GroupNameDialog({
79
79
  <div
80
80
  role="dialog"
81
81
  aria-label="This track cannot be grouped"
82
- className="z-[200] w-64 rounded-md border border-white/10 bg-[#1b1b1f] p-3 text-[11px] leading-snug text-white/75 shadow-xl"
82
+ className="z-200 w-64 rounded-md border border-white/10 bg-[#1b1b1f] p-3 text-[11px] leading-snug text-white/75 shadow-xl"
83
83
  style={{ position: "fixed", ...groupDialogPosition(anchorRect) }}
84
84
  onPointerDown={(event) => event.stopPropagation()}
85
85
  onKeyDown={(event) => {
@@ -97,7 +97,7 @@ function GroupNameDialog({
97
97
  <div
98
98
  role="dialog"
99
99
  aria-label="Name this group"
100
- className="z-[200] w-64 rounded-md border border-white/10 bg-[#1b1b1f] p-3 text-[11px] text-white/75 shadow-xl"
100
+ className="z-200 w-64 rounded-md border border-white/10 bg-[#1b1b1f] p-3 text-[11px] text-white/75 shadow-xl"
101
101
  style={{ position: "fixed", ...groupDialogPosition(anchorRect) }}
102
102
  onPointerDown={(event) => event.stopPropagation()}
103
103
  onKeyDown={(event) => {
@@ -115,7 +115,7 @@ function GroupNameDialog({
115
115
  aria-label="Group name"
116
116
  value={label}
117
117
  onChange={(event) => setLabel(event.currentTarget.value)}
118
- className="w-full rounded border border-white/20 bg-black/30 px-1.5 py-1 text-[11px] text-white outline-none focus:border-[#3CE6AC]"
118
+ className="w-full rounded-sm border border-white/20 bg-black/30 px-1.5 py-1 text-[11px] text-white outline-hidden focus:border-[#3CE6AC]"
119
119
  />
120
120
  {/* The sentence. No jargon, and it names both things a bus does. */}
121
121
  <p className="mt-2 leading-snug">
@@ -125,14 +125,14 @@ function GroupNameDialog({
125
125
  <div className="mt-2.5 flex justify-end gap-1.5">
126
126
  <button
127
127
  type="button"
128
- className="rounded border border-white/20 px-2 py-1 text-[10px] text-white/75 hover:bg-white/10"
128
+ className="rounded-sm border border-white/20 px-2 py-1 text-[10px] text-white/75 hover:bg-white/10"
129
129
  onClick={onCancel}
130
130
  >
131
131
  Cancel
132
132
  </button>
133
133
  <button
134
134
  type="button"
135
- className="rounded border border-[#3CE6AC] bg-[#3CE6AC]/15 px-2 py-1 text-[10px] font-semibold text-[#3CE6AC] hover:bg-[#3CE6AC]/25"
135
+ className="rounded-sm border border-[#3CE6AC] bg-[#3CE6AC]/15 px-2 py-1 text-[10px] font-semibold text-[#3CE6AC] hover:bg-[#3CE6AC]/25"
136
136
  onClick={confirm}
137
137
  >
138
138
  Group
@@ -212,7 +212,7 @@ export function TimelineFxButton(props: TimelineFxButtonProps) {
212
212
  ref={buttonRef}
213
213
  aria-label="Effects — group these clips first"
214
214
  title="Group these clips to add effects to all of them"
215
- className="flex h-6 items-center justify-center rounded border-0 bg-transparent px-1 text-[10px] font-semibold text-white/35 hover:text-white/75"
215
+ className="flex h-6 items-center justify-center rounded-sm border-0 bg-transparent px-1 text-[10px] font-semibold text-white/35 hover:text-white/75"
216
216
  onPointerDown={(event) => event.stopPropagation()}
217
217
  onClick={(event) => {
218
218
  event.stopPropagation();
@@ -2,7 +2,7 @@
2
2
 
3
3
  import React, { act } from "react";
4
4
  import { createRoot } from "react-dom/client";
5
- import { afterEach, describe, expect, it } from "vitest";
5
+ import { afterEach, describe, expect, it, vi } from "vitest";
6
6
  import { defaultTimelineTheme } from "./timelineTheme";
7
7
  import { TimelineGestureOverlay } from "./TimelineGestureOverlay";
8
8
  import type { DraggedClipState } from "./timelineClipDragTypes";
@@ -65,6 +65,40 @@ describe("TimelineGestureOverlay", () => {
65
65
  act(() => root.unmount());
66
66
  });
67
67
 
68
+ it("asks for the same thumbnail frames as the clip at rest", () => {
69
+ const host = document.createElement("div");
70
+ document.body.append(host);
71
+ const root = createRoot(host);
72
+ const renderClipContent = vi.fn(() => null);
73
+ act(() => {
74
+ root.render(
75
+ <TimelineGestureOverlay
76
+ drag={drag}
77
+ scrollRef={{
78
+ current: {
79
+ scrollLeft: 0,
80
+ scrollTop: 0,
81
+ getBoundingClientRect: () => ({ left: 0, top: 0 }),
82
+ } as HTMLDivElement,
83
+ }}
84
+ pixelsPerSecond={100}
85
+ rowHeight={42}
86
+ selectedElementId="hero"
87
+ currentTime={4}
88
+ theme={defaultTimelineTheme}
89
+ getTrackStyle={getTrackStyle}
90
+ renderClipContent={renderClipContent}
91
+ />,
92
+ );
93
+ });
94
+ expect(renderClipContent).toHaveBeenCalledWith(
95
+ expect.anything(),
96
+ expect.anything(),
97
+ expect.objectContaining({ rich: false }),
98
+ );
99
+ act(() => root.unmount());
100
+ });
101
+
68
102
  it("keeps the stable overlay host after terminal cleanup", () => {
69
103
  const host = document.createElement("div");
70
104
  document.body.append(host);
@@ -89,7 +89,7 @@ export const TimelineGestureOverlay = memo(function TimelineGestureOverlay({
89
89
  getTrackStyle(element.tag),
90
90
  renderClipContent,
91
91
  renderClipOverlay,
92
- { priority: "interaction", rich: true },
92
+ { priority: "interaction", rich: false },
93
93
  )}
94
94
  </TimelineClip>
95
95
  </div>
@@ -55,7 +55,7 @@ function GroupNameButton({
55
55
  // No `flex-1`: the row's control group owns the slack now (`ml-auto`), so
56
56
  // claiming it here would push the controls off the right edge — and the
57
57
  // count with them, since it rides inside this button.
58
- className="flex h-6 min-w-0 items-center gap-1.5 rounded border-0 bg-transparent p-0 text-left text-[11px] text-white hover:text-[#3CE6AC] focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC]"
58
+ className="flex h-6 min-w-0 items-center gap-1.5 rounded-sm border-0 bg-transparent p-0 text-left text-[11px] text-white hover:text-[#3CE6AC] focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-[#3CE6AC]"
59
59
  onPointerDown={(event) => event.stopPropagation()}
60
60
  onClick={(event) => {
61
61
  event.stopPropagation();
@@ -67,7 +67,7 @@ function GroupNameButton({
67
67
  </span>
68
68
  {/* Wraps rather than truncating — a name that needs a hover to be read
69
69
  is no use in a column you scan. */}
70
- <span className="min-w-0 break-words text-left font-medium leading-tight">{label}</span>
70
+ <span className="min-w-0 wrap-break-word text-left font-medium leading-tight">{label}</span>
71
71
  <span
72
72
  className="shrink-0 rounded-full bg-white/10 px-1 text-[9px] leading-[14px] tabular-nums text-white/55"
73
73
  aria-hidden="true"
@@ -99,7 +99,7 @@ export function TimelineGroupHeader({
99
99
  <div
100
100
  role="rowheader"
101
101
  aria-colindex={1}
102
- className="sticky left-0 z-[12] flex shrink-0 items-center gap-1.5 overflow-hidden px-1.5 text-[11px]"
102
+ className="sticky left-0 z-12 flex shrink-0 items-center gap-1.5 overflow-hidden px-1.5 text-[11px]"
103
103
  style={{
104
104
  width: columnWidth,
105
105
  height: TRACK_H,
@@ -121,7 +121,7 @@ export function TimelineGroupHeader({
121
121
  // 13px mono, matching the property panel's preset-run caret
122
122
  // (`hf-fx-preset-run-caret`) — the same disclosure, so the same glyph
123
123
  // at the same size rather than a smaller one unique to this row.
124
- className={`flex h-6 w-6 shrink-0 items-center justify-center rounded border-0 bg-transparent p-0 font-mono text-[13px] focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC] ${
124
+ className={`flex h-6 w-6 shrink-0 items-center justify-center rounded border-0 bg-transparent p-0 font-mono text-[13px] focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-[#3CE6AC] ${
125
125
  isExpanded ? "text-white" : "text-white/55 hover:text-white"
126
126
  }`}
127
127
  onPointerDown={(event) => event.stopPropagation()}
@@ -159,7 +159,7 @@ export function TimelineGroupHeader({
159
159
  aria-label={`${isLaneOpen ? "Hide" : "Show"} ${label} lanes`}
160
160
  title={`${isLaneOpen ? "Hide" : "Show"} lanes`}
161
161
  // Anchored right, matching every other header's lane toggle.
162
- className={`ml-auto flex h-6 items-center justify-center gap-0.5 rounded border-0 bg-transparent px-1 text-[11px] leading-none focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC] ${
162
+ className={`ml-auto flex h-6 items-center justify-center gap-0.5 rounded border-0 bg-transparent px-1 text-[11px] leading-none focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-[#3CE6AC] ${
163
163
  isLaneOpen ? "text-[#3CE6AC]" : "text-white/55 hover:text-white"
164
164
  }`}
165
165
  onPointerDown={(event) => event.stopPropagation()}
@@ -64,7 +64,7 @@ export function TimelineGroupLaneLabels({
64
64
  key={lane.target}
65
65
  data-group-lane-label={lane.target}
66
66
  aria-label={`Show ${groupLabel} ${parts.name}${parts.param ? ` ${parts.param}` : ""} in the effect rack`}
67
- className="absolute left-0 flex items-center gap-1.5 overflow-hidden border-0 px-1.5 text-left text-[10px] text-white/65 hover:text-white focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC]"
67
+ className="absolute left-0 flex items-center gap-1.5 overflow-hidden border-0 px-1.5 text-left text-[10px] text-white/65 hover:text-white focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-[#3CE6AC]"
68
68
  style={{
69
69
  top: top + index * AUTOMATION_LANE_H,
70
70
  width: columnWidth,
@@ -140,7 +140,7 @@ export function TimelineGroupRow({
140
140
  around the labels alone does not work either, because as a flex item
141
141
  after the header it starts at x = columnWidth, i.e. inside the lanes. */}
142
142
  <div
143
- className="sticky left-0 z-[12] shrink-0"
143
+ className="sticky left-0 z-12 shrink-0"
144
144
  style={{ width: contentOrigin >= LABEL_COL_W ? LABEL_COL_W : contentOrigin }}
145
145
  >
146
146
  <TimelineGroupHeader