@hyperframes/studio 0.8.50 → 0.8.52

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 (280) hide show
  1. package/dist/assets/{hyperframes-player-CxxDxHTW.js → hyperframes-player-I5YRzB02.js} +3 -3
  2. package/dist/assets/{index-Dt3qBI-9.js → index-BTAfgesM.js} +1 -1
  3. package/dist/assets/index-CxAu_Okg.css +1 -0
  4. package/dist/assets/index-DocJlW2v.js +376 -0
  5. package/dist/assets/{index-DUAIWxkI.js → index-Du5x7KUR.js} +1 -1
  6. package/dist/index.d.ts +419 -20
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +7496 -8288
  9. package/dist/index.js.map +1 -1
  10. package/dist/styles/tailwind-preset.js.map +1 -1
  11. package/package.json +13 -12
  12. package/src/App.tsx +131 -143
  13. package/src/captions/components/CaptionAnimationPanel.tsx +4 -4
  14. package/src/captions/components/CaptionOverlay.sender.test.tsx +52 -0
  15. package/src/captions/components/CaptionOverlay.tsx +8 -4
  16. package/src/captions/components/CaptionOverlayUtils.ts +2 -3
  17. package/src/captions/components/CaptionPropertyPanel.tsx +3 -3
  18. package/src/captions/components/CaptionTimeline.tsx +1 -1
  19. package/src/captions/components/shared.tsx +2 -2
  20. package/src/components/AskAgentModal.tsx +5 -5
  21. package/src/components/EditorShell.tsx +8 -7
  22. package/src/components/ExternalFileConflictBanner.tsx +16 -16
  23. package/src/components/LintModal.tsx +6 -16
  24. package/src/components/MediaPreview.tsx +2 -2
  25. package/src/components/RightPanelTabs.test.tsx +151 -0
  26. package/src/components/RightPanelTabs.tsx +59 -0
  27. package/src/components/SaveQueuePausedBanner.tsx +2 -2
  28. package/src/components/StudioHeader.dom.test.tsx +181 -0
  29. package/src/components/StudioHeader.tsx +110 -210
  30. package/src/components/StudioLeftSidebar.tsx +7 -4
  31. package/src/components/StudioOverlays.tsx +1 -1
  32. package/src/components/StudioRightPanel.tsx +51 -39
  33. package/src/components/StudioToast.tsx +2 -2
  34. package/src/components/TimelineHistoryButtons.dom.test.tsx +117 -0
  35. package/src/components/TimelineHistoryButtons.tsx +54 -0
  36. package/src/components/TimelineToolbar.test.tsx +8 -0
  37. package/src/components/TimelineToolbar.tsx +8 -12
  38. package/src/components/dock/Dock.test.tsx +260 -0
  39. package/src/components/dock/Dock.tsx +281 -0
  40. package/src/components/dock/dock.css +28 -0
  41. package/src/components/dock/dockAccessibility.test.ts +166 -0
  42. package/src/components/dock/dockAccessibility.ts +98 -0
  43. package/src/components/dock/dockLayout.test.ts +134 -0
  44. package/src/components/dock/dockLayout.ts +105 -0
  45. package/src/components/dock/dockLayoutSchema.ts +66 -0
  46. package/src/components/dock/dockLayoutStore.ts +91 -0
  47. package/src/components/dock/dockTestHarness.ts +11 -0
  48. package/src/components/dock/panelRegistry.ts +67 -0
  49. package/src/components/editor/AnimationCard.tsx +4 -4
  50. package/src/components/editor/AnimationCardParts.tsx +6 -6
  51. package/src/components/editor/BlockParamsPanel.tsx +4 -4
  52. package/src/components/editor/BorderRadiusEditor.tsx +1 -1
  53. package/src/components/editor/CanvasContextMenu.tsx +1 -1
  54. package/src/components/editor/ComputedTweenNotice.tsx +1 -1
  55. package/src/components/editor/DomEditOverlay.tsx +1 -1
  56. package/src/components/editor/EaseCurveSection.tsx +1 -1
  57. package/src/components/editor/EaseParamFields.tsx +5 -5
  58. package/src/components/editor/FileTree.tsx +4 -4
  59. package/src/components/editor/FileTreeIcons.tsx +1 -1
  60. package/src/components/editor/FileTreeNodes.tsx +13 -13
  61. package/src/components/editor/InlineTextToolbar.tsx +1 -1
  62. package/src/components/editor/InspectorHeaderActions.tsx +3 -3
  63. package/src/components/editor/KeyframeDiamond.tsx +1 -1
  64. package/src/components/editor/KeyframeEaseList.tsx +1 -1
  65. package/src/components/editor/LayersPanel.test.ts +97 -1
  66. package/src/components/editor/LayersPanel.tsx +7 -5
  67. package/src/components/editor/MarqueeOverlay.tsx +1 -1
  68. package/src/components/editor/OffCanvasIndicators.tsx +1 -1
  69. package/src/components/editor/PreviewGuides.tsx +1 -1
  70. package/src/components/editor/PromotableControl.tsx +2 -2
  71. package/src/components/editor/PropertyPanelEmptyState.tsx +7 -7
  72. package/src/components/editor/PropertyPanelFlatHeader.tsx +2 -2
  73. package/src/components/editor/SnapToolbar.tsx +2 -2
  74. package/src/components/editor/TimelineFxPopover.tsx +1 -1
  75. package/src/components/editor/TopologyLens.test.tsx +28 -0
  76. package/src/components/editor/TopologyLens.tsx +7 -5
  77. package/src/components/editor/Transform3DCube.tsx +2 -2
  78. package/src/components/editor/audioFxRevealTarget.ts +1 -0
  79. package/src/components/editor/domEditing.test.ts +1 -1
  80. package/src/components/editor/domEditingAgentPrompt.test.ts +8 -6
  81. package/src/components/editor/domEditingAgentPrompt.ts +4 -3
  82. package/src/components/editor/propertyPanelColor.tsx +11 -11
  83. package/src/components/editor/propertyPanelColorCurveGraph.tsx +1 -1
  84. package/src/components/editor/propertyPanelColorGradingControls.tsx +7 -9
  85. package/src/components/editor/propertyPanelColorGradingSection.tsx +6 -6
  86. package/src/components/editor/propertyPanelColorGradingSlider.tsx +5 -5
  87. package/src/components/editor/propertyPanelColorWheels.tsx +1 -1
  88. package/src/components/editor/propertyPanelCommitField.tsx +1 -1
  89. package/src/components/editor/propertyPanelFill.tsx +4 -4
  90. package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +3 -3
  91. package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +7 -7
  92. package/src/components/editor/propertyPanelFlatEffectsSection.tsx +4 -4
  93. package/src/components/editor/propertyPanelFlatMediaSection.tsx +3 -3
  94. package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +1 -1
  95. package/src/components/editor/propertyPanelFlatPrimitives.tsx +10 -10
  96. package/src/components/editor/propertyPanelFlatSelectRow.tsx +3 -3
  97. package/src/components/editor/propertyPanelFlatTextSection.test.tsx +8 -9
  98. package/src/components/editor/propertyPanelFlatTextSection.tsx +5 -5
  99. package/src/components/editor/propertyPanelFlatToggle.tsx +1 -1
  100. package/src/components/editor/propertyPanelFont.tsx +5 -5
  101. package/src/components/editor/propertyPanelFxCarveModule.tsx +1 -1
  102. package/src/components/editor/propertyPanelFxControls.test.tsx +1 -1
  103. package/src/components/editor/propertyPanelFxControls.tsx +6 -6
  104. package/src/components/editor/propertyPanelFxEqModule.tsx +2 -2
  105. package/src/components/editor/propertyPanelFxPresetStyle.ts +2 -2
  106. package/src/components/editor/propertyPanelGradingNumberField.tsx +1 -1
  107. package/src/components/editor/propertyPanelMediaSection.tsx +1 -1
  108. package/src/components/editor/propertyPanelPrimitives.tsx +7 -7
  109. package/src/components/editor/propertyPanelSections.tsx +8 -8
  110. package/src/components/editor/useColorGradingController.test.ts +38 -0
  111. package/src/components/editor/useColorGradingController.ts +9 -1
  112. package/src/components/editor/useColorGradingPreviews.test.ts +76 -0
  113. package/src/components/editor/useColorGradingPreviews.ts +11 -1
  114. package/src/components/editor/usePreviewCompositionRect.test.tsx +95 -0
  115. package/src/components/editor/usePreviewCompositionRect.ts +21 -0
  116. package/src/components/feedback/StudioFeedbackCard.tsx +7 -7
  117. package/src/components/feedback/feedbackTrigger.ts +0 -1
  118. package/src/components/nle/AssetPreviewOverlay.tsx +6 -2
  119. package/src/components/nle/CompositionBreadcrumb.tsx +3 -3
  120. package/src/components/nle/NLEPreview.tsx +3 -3
  121. package/src/components/nle/PreviewOverlays.tsx +6 -6
  122. package/src/components/nle/PreviewPane.tsx +3 -3
  123. package/src/components/nle/TimelinePane.tsx +3 -3
  124. package/src/components/nle/TimelineResizeDivider.tsx +2 -2
  125. package/src/components/panels/SlideshowPanel.tsx +2 -2
  126. package/src/components/panels/SlideshowSubPanels.tsx +20 -17
  127. package/src/components/panels/VariablesBindElement.tsx +2 -2
  128. package/src/components/panels/VariablesDeclarationForm.tsx +2 -2
  129. package/src/components/panels/VariablesOtherCompositions.tsx +1 -1
  130. package/src/components/panels/VariablesPanel.tsx +3 -3
  131. package/src/components/panels/VariablesValueControls.tsx +2 -2
  132. package/src/components/pointerTargetSize.test.tsx +8 -0
  133. package/src/components/renders/FfmpegRequiredNotice.tsx +5 -5
  134. package/src/components/renders/RenderQueue.test.tsx +7 -0
  135. package/src/components/renders/RenderQueue.tsx +7 -7
  136. package/src/components/renders/RenderQueueItem.tsx +6 -6
  137. package/src/components/sidebar/AssetCard.tsx +8 -8
  138. package/src/components/sidebar/AssetContextMenu.tsx +8 -8
  139. package/src/components/sidebar/AssetsTab.tsx +2 -2
  140. package/src/components/sidebar/AudioRow.tsx +6 -6
  141. package/src/components/sidebar/BlocksTab.tsx +6 -6
  142. package/src/components/sidebar/CompositionsTab.tsx +5 -5
  143. package/src/components/sidebar/GlobalAssetsView.tsx +1 -1
  144. package/src/components/sidebar/LeftSidebar.lintBadge.test.tsx +58 -0
  145. package/src/components/sidebar/LeftSidebar.tsx +19 -5
  146. package/src/components/sidebar/PromptPreviewModal.tsx +3 -3
  147. package/src/components/timelineToolbarStyles.ts +5 -0
  148. package/src/components/ui/Button.test.tsx +252 -0
  149. package/src/components/ui/Button.tsx +89 -126
  150. package/src/components/ui/IconButton.tsx +38 -0
  151. package/src/components/ui/Input.tsx +115 -0
  152. package/src/components/ui/Menu.test.tsx +374 -0
  153. package/src/components/ui/Menu.tsx +213 -0
  154. package/src/components/ui/NumberField.tsx +122 -0
  155. package/src/components/ui/Popover.tsx +74 -0
  156. package/src/components/ui/SearchInput.tsx +2 -2
  157. package/src/components/ui/Select.tsx +100 -0
  158. package/src/components/ui/Slider.tsx +153 -0
  159. package/src/components/ui/Tabs.test.tsx +98 -0
  160. package/src/components/ui/Tabs.tsx +71 -0
  161. package/src/components/ui/Toggle.tsx +63 -0
  162. package/src/components/ui/Tooltip.test.tsx +110 -0
  163. package/src/components/ui/Tooltip.tsx +38 -112
  164. package/src/components/ui/cn.test.ts +30 -0
  165. package/src/components/ui/cn.ts +25 -0
  166. package/src/components/ui/index.ts +29 -2
  167. package/src/components/ui/mountHost.testHelpers.ts +24 -0
  168. package/src/components/ui/valueControls.test.tsx +524 -0
  169. package/src/hooks/useAppHotkeys.ts +21 -80
  170. package/src/hooks/useCaptionDetection.sender.test.tsx +46 -0
  171. package/src/hooks/useCaptionDetection.ts +3 -1
  172. package/src/hooks/useEditHistoryActions.test.tsx +90 -0
  173. package/src/hooks/useEditHistoryActions.ts +106 -0
  174. package/src/hooks/useLintModal.test.tsx +65 -0
  175. package/src/hooks/useLintModal.ts +3 -0
  176. package/src/hooks/useSdkSession.lifecycle.test.tsx +49 -4
  177. package/src/hooks/useSdkSession.ts +43 -10
  178. package/src/hooks/useServerConnection.hashProject.test.tsx +94 -0
  179. package/src/hooks/useServerConnection.ts +43 -10
  180. package/src/hooks/useTimelineAssetDropOps.test.tsx +41 -1
  181. package/src/hooks/useTimelineAssetDropOps.ts +52 -11
  182. package/src/index.ts +51 -0
  183. package/src/main.tsx +9 -3
  184. package/src/player/components/AudioWaveform.tsx +1 -1
  185. package/src/player/components/AutomationSelectionMenu.tsx +3 -3
  186. package/src/player/components/ClipContextMenu.tsx +3 -3
  187. package/src/player/components/CompositionThumbnail.test.ts +0 -3
  188. package/src/player/components/CompositionThumbnail.tsx +3 -4
  189. package/src/player/components/EditModal.tsx +1 -1
  190. package/src/player/components/ImageThumbnail.tsx +1 -1
  191. package/src/player/components/KeyframeDiamondContextMenu.tsx +4 -4
  192. package/src/player/components/LayerDisclosureRow.tsx +2 -2
  193. package/src/player/components/PlayerControls.tsx +1 -1
  194. package/src/player/components/ShortcutsPanel.tsx +9 -9
  195. package/src/player/components/SpeedMenu.tsx +3 -5
  196. package/src/player/components/Timeline.tsx +2 -1
  197. package/src/player/components/TimelineCanvas.tsx +21 -14
  198. package/src/player/components/TimelineClip.tsx +0 -1
  199. package/src/player/components/TimelineDiamondConnectors.tsx +1 -1
  200. package/src/player/components/TimelineEmptyState.tsx +5 -5
  201. package/src/player/components/TimelineFxButton.tsx +6 -6
  202. package/src/player/components/TimelineGroupHeader.tsx +5 -5
  203. package/src/player/components/TimelineGroupLaneLabels.tsx +1 -1
  204. package/src/player/components/TimelineGroupRow.tsx +1 -1
  205. package/src/player/components/TimelineLanes.test.tsx +32 -0
  206. package/src/player/components/TimelineLanes.tsx +2 -5
  207. package/src/player/components/TimelineLayerGroupHeader.tsx +2 -2
  208. package/src/player/components/TimelineRuler.tsx +1 -1
  209. package/src/player/components/TimelineShortcutHint.tsx +1 -1
  210. package/src/player/components/TimelineTrackHeader.tsx +1 -1
  211. package/src/player/components/TimelineTrackPlainHeader.tsx +3 -3
  212. package/src/player/components/TimelineTypes.ts +2 -1
  213. package/src/player/components/TrackGapContextMenu.tsx +1 -1
  214. package/src/player/components/VideoThumbnail.tsx +4 -2
  215. package/src/player/components/VolumeControl.tsx +2 -2
  216. package/src/player/components/timelineCallbacks.ts +8 -8
  217. package/src/player/components/timelineClipDragPreview.ts +51 -27
  218. package/src/player/components/timelineClipDragTypes.ts +3 -0
  219. package/src/player/components/timelineDragDrop.test.tsx +61 -1
  220. package/src/player/components/timelineDragDrop.ts +57 -15
  221. package/src/player/components/timelineLaneProps.ts +3 -0
  222. package/src/player/components/timelineSnapping.test.ts +27 -0
  223. package/src/player/components/timelineSnapping.ts +14 -1
  224. package/src/player/components/trackHeaderLabelRows.tsx +4 -4
  225. package/src/player/components/useTimelineClipDrag.resize.test.tsx +136 -2
  226. package/src/player/components/useTimelineClipDrag.ts +30 -3
  227. package/src/player/lib/describeClips.ts +3 -54
  228. package/src/player/lib/playbackShortcuts.ts +2 -0
  229. package/src/player/store/previewIframeStore.notify.test.tsx +75 -0
  230. package/src/player/store/previewIframeStore.test.tsx +29 -0
  231. package/src/player/store/previewIframeStore.ts +25 -0
  232. package/src/renderQueueExports.test.tsx +58 -0
  233. package/src/styles/classCandidates.test.ts +105 -0
  234. package/src/styles/classCandidates.ts +281 -0
  235. package/src/styles/hex-baseline.json +84 -0
  236. package/src/styles/hexRatchet.test.ts +201 -0
  237. package/src/styles/iconTokens.ts +20 -0
  238. package/src/styles/palette-reset.css +6 -0
  239. package/src/styles/studio.css +24 -21
  240. package/src/styles/styleSources.test.ts +20 -0
  241. package/src/styles/styleSources.ts +47 -0
  242. package/src/styles/tailwind-preset.shared.js +3 -0
  243. package/src/styles/theme.css +243 -0
  244. package/src/styles/theme.test.ts +183 -0
  245. package/src/styles/tokenGate.test.ts +109 -0
  246. package/src/utils/sourcePatcher.ts +1 -1
  247. package/src/utils/studioHelpers.test.ts +18 -0
  248. package/src/utils/studioHelpers.ts +11 -1
  249. package/src/utils/studioUiPreferences.ts +7 -0
  250. package/src/utils/timelineDropTrackInsert.test.ts +134 -0
  251. package/src/utils/timelineDropTrackInsert.ts +83 -0
  252. package/src/utils/typingTarget.ts +4 -0
  253. package/dist/assets/index-n2ml4tUn.js +0 -442
  254. package/dist/assets/index-pO6p0H27.css +0 -1
  255. package/src/components/PanelTabButton.tsx +0 -31
  256. package/src/components/storyboard/AgentChatMessageButton.test.tsx +0 -47
  257. package/src/components/storyboard/AgentChatMessageButton.tsx +0 -45
  258. package/src/components/storyboard/FramePoster.test.tsx +0 -61
  259. package/src/components/storyboard/FramePoster.tsx +0 -86
  260. package/src/components/storyboard/StoryboardDirection.tsx +0 -45
  261. package/src/components/storyboard/StoryboardFrameFocus.tsx +0 -412
  262. package/src/components/storyboard/StoryboardFrameTile.tsx +0 -117
  263. package/src/components/storyboard/StoryboardGrid.tsx +0 -55
  264. package/src/components/storyboard/StoryboardLoaded.tsx +0 -307
  265. package/src/components/storyboard/StoryboardReviewGuide.tsx +0 -300
  266. package/src/components/storyboard/StoryboardScriptPanel.tsx +0 -26
  267. package/src/components/storyboard/StoryboardSourceEditor.tsx +0 -231
  268. package/src/components/storyboard/StoryboardStatusLegend.tsx +0 -21
  269. package/src/components/storyboard/StoryboardView.tsx +0 -169
  270. package/src/components/storyboard/StoryboardViewModeGuard.test.tsx +0 -187
  271. package/src/components/storyboard/frameComments.test.ts +0 -95
  272. package/src/components/storyboard/frameComments.ts +0 -115
  273. package/src/components/storyboard/frameStatus.ts +0 -36
  274. package/src/components/storyboard/storyboardReviewStage.test.ts +0 -40
  275. package/src/components/storyboard/storyboardReviewStage.ts +0 -45
  276. package/src/components/storyboard/useFrameComments.ts +0 -98
  277. package/src/contexts/ViewModeContext.tsx +0 -142
  278. package/src/hooks/useProjectSignaturePoll.test.tsx +0 -135
  279. package/src/hooks/useProjectSignaturePoll.ts +0 -68
  280. package/src/hooks/useStoryboard.ts +0 -99
@@ -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
@@ -233,6 +233,7 @@ export const Timeline = memo(function Timeline({
233
233
  onResizeElement: pinnedOnResizeElement,
234
234
  onResizeElements: pinnedOnResizeElements,
235
235
  onBlockedEditAttempt,
236
+ onSeek,
236
237
  setShowPopover,
237
238
  setRangeSelectionRef,
238
239
  readZIndex: zSyncEnabled ? readClipZIndex : undefined,
@@ -464,7 +465,7 @@ export const Timeline = memo(function Timeline({
464
465
  data-timeline-scroll-viewport
465
466
  data-timeline-auto-scroll-left-inset={labelMode ? LABEL_COL_W : 0}
466
467
  tabIndex={-1}
467
- 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`}
468
469
  onScroll={(e) => {
469
470
  lastScrollLeftRef.current = e.currentTarget.scrollLeft; // restored across post-edit reload
470
471
  recordTimelineScroll(e.currentTarget);
@@ -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[];
@@ -40,14 +42,15 @@ interface TimelineCanvasProps extends TimelineLaneBaseProps {
40
42
  /** Gap strips: loud on gap-menu-row hover, quiet on the selected clip's lane. */
41
43
  laneGapStrips: TimelineLaneGapStrips[];
42
44
  /** Landing spot of an outside drag in progress, or null. */
43
- dropPreview: { start: number; track: number } | null;
45
+ dropPreview: TimelineDropPlacement | null;
44
46
  }
45
47
 
46
48
  // A dropped clip's length is unknown until it lands; the preview shows a default.
47
49
  const DROP_PREVIEW_SECONDS = 3;
48
50
 
49
51
  export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvasProps) {
50
- const { draggedClip, scrollRef, selectedElementIds, displayTrackOrder } = props;
52
+ const { draggedClip, resizingClip, scrollRef, selectedElementIds, displayTrackOrder } = props;
53
+ const snapGuide = resolveSnapGuide(draggedClip, resizingClip);
51
54
  const draggedRowIndex =
52
55
  draggedClip?.started === true ? displayTrackOrder.indexOf(draggedClip.previewTrack) : -1;
53
56
  const dropTrackIndex = props.dropPreview
@@ -55,6 +58,8 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
55
58
  : -1;
56
59
  // A track not in the order is a new track, drawn one row past the last lane.
57
60
  const dropRowIndex = dropTrackIndex < 0 ? displayTrackOrder.length : dropTrackIndex;
61
+ const insertLineRow =
62
+ (draggedClip?.started ? draggedClip.insertRow : null) ?? props.dropPreview?.insertRow ?? null;
58
63
  const draggedRowHeight = getTimelineRowHeight(draggedRowIndex, props.rowHeights);
59
64
  // A clip bar in an EXPANDED row still renders at TRACK_H (the property lanes
60
65
  // occupy the rest of the row — see TimelineLanes' clipHeight), so the drag
@@ -117,6 +122,7 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
117
122
 
118
123
  <TimelineLanes
119
124
  {...props}
125
+ snapGuide={snapGuide}
120
126
  draggedElement={draggedElement}
121
127
  multiDragPreview={multiDragPreview}
122
128
  onToggleTrackHidden={onToggleTrackHidden}
@@ -173,8 +179,8 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
173
179
  left: props.contentOrigin + draggedClip.previewStart * props.pps,
174
180
  width: Math.max(draggedClip.element.duration * props.pps, 4),
175
181
  height: draggedClipHeight,
176
- border: "1px solid rgba(60,230,172,0.55)",
177
- background: "rgba(60,230,172,0.12)",
182
+ border: "1px solid color-mix(in srgb, var(--color-accent) 55%, transparent)",
183
+ background: "color-mix(in srgb, var(--color-accent) 12%, transparent)",
178
184
  borderRadius: 4,
179
185
  zIndex: 30,
180
186
  }}
@@ -183,7 +189,7 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
183
189
 
184
190
  {/* Drop preview: where an asset or file dragged in from outside will land
185
191
  (a row past the last lane means a new track). */}
186
- {props.dropPreview && (
192
+ {props.dropPreview && props.dropPreview.insertRow == null && (
187
193
  <div
188
194
  aria-hidden="true"
189
195
  data-testid="timeline-drop-preview"
@@ -193,8 +199,8 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
193
199
  left: props.contentOrigin + props.dropPreview.start * props.pps,
194
200
  width: DROP_PREVIEW_SECONDS * props.pps,
195
201
  height: TRACK_H - CLIP_Y * 2,
196
- border: "1px solid rgba(60,230,172,0.55)",
197
- background: "rgba(60,230,172,0.12)",
202
+ border: "1px solid color-mix(in srgb, var(--color-accent) 55%, transparent)",
203
+ background: "color-mix(in srgb, var(--color-accent) 12%, transparent)",
198
204
  borderRadius: 4,
199
205
  zIndex: 30,
200
206
  }}
@@ -203,11 +209,12 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
203
209
 
204
210
  {/* Insertion line — a new track will be inserted at this boundary on drop.
205
211
  Shown while the pointer is near a lane boundary (insert mode). */}
206
- {draggedClip?.started && draggedClip.insertRow != null && (
212
+ {insertLineRow != null && (
207
213
  <div
214
+ data-testid="timeline-insert-line"
208
215
  className="absolute pointer-events-none"
209
216
  style={{
210
- top: getTimelineRowTop(draggedClip.insertRow, props.rowHeights) - 0.5,
217
+ top: getTimelineRowTop(insertLineRow, props.rowHeights) - 0.5,
211
218
  left: props.contentOrigin,
212
219
  width: props.trackContentWidth,
213
220
  height: 1,
@@ -218,18 +225,18 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
218
225
  />
219
226
  )}
220
227
 
221
- {/* Snap guide for non-beat targets during clip drag */}
222
- {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" && (
223
230
  <div
224
231
  className="absolute pointer-events-none"
225
232
  style={{
226
- left: props.contentOrigin + draggedClip.snapTime * props.pps,
233
+ left: props.contentOrigin + snapGuide.time * props.pps,
227
234
  top: RULER_H,
228
235
  bottom: 0,
229
236
  width: 1,
230
- background: draggedClip.snapType === "playhead" ? "#3CE6AC" : "rgba(255,255,255,0.6)",
237
+ background: snapGuide.type === "playhead" ? "#3CE6AC" : "rgba(255,255,255,0.6)",
231
238
  boxShadow:
232
- draggedClip.snapType === "playhead"
239
+ snapGuide.type === "playhead"
233
240
  ? "0 0 6px rgba(60,230,172,0.5)"
234
241
  : "0 0 6px rgba(255,255,255,0.4)",
235
242
  zIndex: 60,
@@ -71,7 +71,6 @@ export const TimelineClip = memo(function TimelineClip({
71
71
  isSelected ? "is-selected" : "",
72
72
  isHovered ? "is-hovered" : "",
73
73
  isDragging ? "is-dragging" : "",
74
- showDefaultText ? "" : "is-micro",
75
74
  isAudioTimelineElement(el) ? "is-audio" : "",
76
75
  ]
77
76
  .filter((className) => className.length > 0)
@@ -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();
@@ -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
@@ -72,6 +72,7 @@ interface RenderLanesOptions {
72
72
  onContextMenuLane?: (e: React.MouseEvent, track: number, time: number) => void;
73
73
  hoveredClip?: string | null;
74
74
  renderClipContent?: React.ComponentProps<typeof TimelineLanes>["renderClipContent"];
75
+ snapGuide?: { time: number; type: "beat" | "clip-edge" | "playhead" } | null;
75
76
  }
76
77
 
77
78
  function renderLanes(options: RenderLanesOptions = {}): {
@@ -141,6 +142,7 @@ function renderLanes(options: RenderLanesOptions = {}): {
141
142
  renderClipContent={next.renderClipContent}
142
143
  draggedClip={next.draggedClip ?? null}
143
144
  draggedElement={null}
145
+ snapGuide={next.snapGuide ?? null}
144
146
  multiDragPreview={next.multiDragPreview ?? null}
145
147
  blockedClipRef={createRef<BlockedClipState | null>()}
146
148
  suppressClickRef={{ current: false }}
@@ -179,6 +181,36 @@ function visibilityLabels(host: HTMLElement): (string | null)[] {
179
181
  );
180
182
  }
181
183
 
184
+ /** The beat guide's own highlight div, keyed by the green glow every other beat lacks. */
185
+ function beatHighlight(host: HTMLElement): HTMLElement | undefined {
186
+ return Array.from(host.querySelectorAll("div")).find((div) =>
187
+ (div.style.boxShadow ?? "").includes("34,197,94"),
188
+ );
189
+ }
190
+
191
+ describe("TimelineLanes beat guide", () => {
192
+ it("draws the beat highlight from snapGuide, not from the stale draggedClip prop", () => {
193
+ const view = renderLanes({
194
+ elements: [element("clip-a", TRACK_A)],
195
+ snapGuide: { time: 1.5, type: "beat" },
196
+ });
197
+
198
+ expect(beatHighlight(view.host)?.style.left).toBe("150px");
199
+ act(() => view.root.unmount());
200
+ });
201
+
202
+ it("clears the highlight once the trim it belonged to ends", () => {
203
+ const view = renderLanes({
204
+ elements: [element("clip-a", TRACK_A)],
205
+ snapGuide: { time: 1.5, type: "beat" },
206
+ });
207
+ view.rerender({ elements: [element("clip-a", TRACK_A)], snapGuide: null });
208
+
209
+ expect(beatHighlight(view.host)).toBeUndefined();
210
+ act(() => view.root.unmount());
211
+ });
212
+ });
213
+
182
214
  describe("TimelineLanes track numbering", () => {
183
215
  // Screen readers literally announced "Hide track 0.16666666666666666".
184
216
  it("numbers tracks contiguously from 1 regardless of the fractional sort keys", () => {
@@ -59,6 +59,7 @@ export function TimelineLanes({
59
59
  hoveredClip,
60
60
  draggedClip,
61
61
  draggedElement,
62
+ snapGuide,
62
63
  multiDragPreview,
63
64
  blockedClipRef,
64
65
  suppressClickRef,
@@ -331,11 +332,7 @@ export function TimelineLanes({
331
332
  beatTimes={beatAnalysis?.beatTimes}
332
333
  beatStrengths={beatAnalysis?.beatStrengths}
333
334
  pps={pps}
334
- highlightTime={
335
- draggedClip?.started && draggedClip.snapType === "beat"
336
- ? draggedClip.snapTime
337
- : null
338
- }
335
+ highlightTime={snapGuide?.type === "beat" ? snapGuide.time : null}
339
336
  renderTimeRange={rowsVirtualized ? renderTimeRange : undefined}
340
337
  />
341
338
  {/* Beat dots on the active track (the one holding the selection),
@@ -56,14 +56,14 @@ export function TimelineLayerGroupHeader({
56
56
  }}
57
57
  >
58
58
  <div
59
- className="sticky left-0 z-[13] flex h-full items-center"
59
+ className="sticky left-0 z-13 flex h-full items-center"
60
60
  style={{
61
61
  width: Math.min(GUTTER + 220, GUTTER + trackContentWidth),
62
62
  background: theme.gutterBackground,
63
63
  }}
64
64
  >
65
65
  <div
66
- className="relative h-full flex-shrink-0"
66
+ className="relative h-full shrink-0"
67
67
  style={{
68
68
  width: GUTTER,
69
69
  borderRight: `1px solid ${theme.gutterBorder}`,
@@ -85,7 +85,7 @@ export const TimelineRuler = memo(function TimelineRuler({
85
85
  style={{ height: RULER_H, width: contentOrigin + trackContentWidth, zIndex: 70 }}
86
86
  >
87
87
  <div
88
- className="sticky left-0 z-[12] flex-shrink-0"
88
+ className="sticky left-0 z-12 shrink-0"
89
89
  style={{
90
90
  width: contentOrigin,
91
91
  // Ruler corner uses the panel surface — same as the ruler strip itself.
@@ -12,7 +12,7 @@ export function TimelineShortcutHint({ theme }: TimelineShortcutHintProps) {
12
12
  style={{ background: "rgba(17,23,35,0.84)", borderColor: theme.gutterBorder }}
13
13
  >
14
14
  <kbd
15
- className="text-[9px] font-mono px-1 py-0.5 rounded"
15
+ className="text-[9px] font-mono px-1 py-0.5 rounded-sm"
16
16
  style={{ color: theme.textSecondary, background: "rgba(255,255,255,0.06)" }}
17
17
  >
18
18
  Shift
@@ -259,7 +259,7 @@ export function TimelineTrackHeader({
259
259
  <div
260
260
  role="rowheader"
261
261
  aria-colindex={1}
262
- className="sticky left-0 z-[12] shrink-0"
262
+ className="sticky left-0 z-12 shrink-0"
263
263
  style={{
264
264
  width: showTrackLabel ? LABEL_COL_W : contentOrigin,
265
265
  background: gutterFill(theme.gutterBackground, isGroupMember),
@@ -39,7 +39,7 @@ export function VisibilityButton({
39
39
  type="button"
40
40
  aria-label={label}
41
41
  title={label}
42
- className={`flex h-6 w-6 shrink-0 items-center justify-center rounded border-0 bg-transparent p-0 transition-colors focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-[-1px] focus-visible:outline-[#3CE6AC] ${
42
+ className={`flex h-6 w-6 shrink-0 items-center justify-center rounded border-0 bg-transparent p-0 transition-colors focus-visible:outline-solid focus-visible:outline-1 focus-visible:-outline-offset-1 focus-visible:outline-[#3CE6AC] ${
43
43
  hidden ? "text-[#3CE6AC] hover:text-white" : "text-white/35 hover:text-white/75"
44
44
  }`}
45
45
  onPointerDown={(event) => event.stopPropagation()}
@@ -97,10 +97,10 @@ export function PlainTrackHeader({
97
97
 
98
98
  Wraps rather than truncating: a truncated name needs a hover to be
99
99
  read at all, and a tooltip is no use to a name you are scanning a
100
- column of. `break-words` so a long single token breaks instead of
100
+ column of. `wrap-break-word` so a long single token breaks instead of
101
101
  forcing the column wider. */}
102
102
  {showTrackLabel && (
103
- <span className="min-w-0 break-words text-[11px] leading-tight">{trackLabel}</span>
103
+ <span className="min-w-0 wrap-break-word text-[11px] leading-tight">{trackLabel}</span>
104
104
  )}
105
105
  {showTrackLabel && <TrackClipCount clipCount={clipCount} />}
106
106
  {/* `ml-auto` is what anchors the group right: it absorbs the slack the
@@ -12,7 +12,8 @@ export interface TimelineClipRenderContext {
12
12
  export interface TimelineProps extends TimelineDropCallbacks, TimelineEditOverrides {
13
13
  /** Project-scoped reset boundary; soft source refreshes retain the same epoch. */
14
14
  sessionEpoch?: number;
15
- onSeek?: (time: number) => void;
15
+ /** keepPlaying: true preserves the current play state across the seek. */
16
+ onSeek?: (time: number, options?: { keepPlaying?: boolean }) => void;
16
17
  onDrillDown?: (element: TimelineElement) => void;
17
18
  renderClipContent?: (
18
19
  element: TimelineElement,
@@ -75,7 +75,7 @@ export const TrackGapContextMenu = memo(function TrackGapContextMenu({
75
75
  return createPortal(
76
76
  <div
77
77
  ref={menuRef}
78
- className="fixed z-[200] bg-neutral-900 border border-neutral-700 rounded-md shadow-lg py-1 min-w-[180px]"
78
+ className="fixed z-200 bg-neutral-900 border border-neutral-700 rounded-md shadow-lg py-1 min-w-[180px]"
79
79
  style={{ left: adjustedX, top: adjustedY }}
80
80
  onPointerLeave={() => onHoverAction(null)}
81
81
  >
@@ -94,7 +94,7 @@ export const VideoThumbnail = memo(function VideoThumbnail({
94
94
  return (
95
95
  <div
96
96
  key={index}
97
- className="relative h-full flex-shrink-0 overflow-hidden bg-neutral-900"
97
+ className="relative h-full shrink-0 overflow-hidden bg-neutral-900"
98
98
  style={{ width: frameW }}
99
99
  >
100
100
  <img
@@ -119,7 +119,9 @@ export const VideoThumbnail = memo(function VideoThumbnail({
119
119
  )}
120
120
  {snapshot.status === "error" && (
121
121
  <div className="absolute inset-0 flex items-center justify-center bg-neutral-900/60">
122
- <span className="rounded bg-black/50 px-1 text-[8px] text-neutral-500">no preview</span>
122
+ <span className="rounded-sm bg-black/50 px-1 text-[8px] text-neutral-500">
123
+ no preview
124
+ </span>
123
125
  </div>
124
126
  )}
125
127
  {label && (
@@ -52,7 +52,7 @@ export const VolumeControl = memo(function VolumeControl({
52
52
  const muteLabel = silent ? "Unmute audio" : "Mute audio";
53
53
 
54
54
  return (
55
- <div className="group flex flex-shrink-0 items-center">
55
+ <div className="group flex shrink-0 items-center">
56
56
  <div className="w-0 overflow-hidden opacity-0 transition-[width,opacity] duration-150 ease-out group-hover:w-14 group-hover:opacity-100 group-focus-within:w-14 group-focus-within:opacity-100">
57
57
  <div className="relative mx-1 flex h-6 w-12 items-center">
58
58
  <div className="absolute inset-x-0 h-0.5 overflow-hidden rounded-full bg-neutral-700">
@@ -92,7 +92,7 @@ export const VolumeControl = memo(function VolumeControl({
92
92
  disabled={disabled}
93
93
  aria-label={muteLabel}
94
94
  aria-pressed={silent}
95
- className={`flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-md transition-colors disabled:pointer-events-none disabled:opacity-30 ${
95
+ className={`flex h-7 w-7 shrink-0 items-center justify-center rounded-md transition-colors disabled:pointer-events-none disabled:opacity-30 ${
96
96
  silent ? "text-studio-accent" : "text-neutral-500 hover:text-neutral-200"
97
97
  }`}
98
98
  >
@@ -14,20 +14,20 @@ export interface TimelinePropertyGroupKeyframeToggle {
14
14
  remove: boolean;
15
15
  }
16
16
 
17
+ /** Where an outside drop lands; `insertRow` opens a new track at that row boundary of `trackOrder`. */
18
+ export type TimelineDropPlacement = { start: number; track: number } & (
19
+ | { insertRow?: null; trackOrder?: undefined }
20
+ | { insertRow: number; trackOrder: readonly number[] }
21
+ );
22
+
17
23
  /**
18
24
  * Shared callback signatures for timeline editing operations.
19
25
  * Used by NLELayout, Timeline, and any component that passes through
20
26
  * the standard set of timeline mutation handlers.
21
27
  */
22
28
  export interface TimelineDropCallbacks {
23
- onFileDrop?: (
24
- files: File[],
25
- placement?: { start: number; track: number },
26
- ) => Promise<void> | void;
27
- onAssetDrop?: (
28
- assetPath: string,
29
- placement: { start: number; track: number },
30
- ) => Promise<void> | void;
29
+ onFileDrop?: (files: File[], placement?: TimelineDropPlacement) => Promise<void> | void;
30
+ onAssetDrop?: (assetPath: string, placement: TimelineDropPlacement) => Promise<void> | void;
31
31
  onBlockDrop?: (
32
32
  blockName: string,
33
33
  placement: { start: number; track: number },