@bycrux/editor 0.11.2 → 1.0.1

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 (268) hide show
  1. package/package.json +3 -1
  2. package/src/ControlsInfoModal.tsx +251 -61
  3. package/src/__tests__/ControlsInfoModal.test.tsx +43 -0
  4. package/src/__tests__/adapter.test.ts +59 -1
  5. package/src/__tests__/schema-assignability.test.ts +93 -0
  6. package/src/__tests__/schema.test.ts +15 -0
  7. package/src/__tests__/video-adapter-contract.test.ts +128 -5
  8. package/src/carousel/AddElementMenu.tsx +10 -4
  9. package/src/carousel/CarouselEditor.tsx +46 -8
  10. package/src/carousel/CarouselRenderModal.tsx +15 -10
  11. package/src/carousel/OverlayPicker.tsx +10 -3
  12. package/src/carousel/SlidePropertyPanel.tsx +43 -21
  13. package/src/components/FilmstripScrubber.tsx +277 -0
  14. package/src/components/__tests__/FilmstripScrubber.test.tsx +216 -0
  15. package/src/engine/__tests__/audio-clock.test.ts +655 -0
  16. package/src/engine/__tests__/audio-worklet-source.test.ts +316 -0
  17. package/src/engine/__tests__/batch-planner.test.ts +298 -0
  18. package/src/engine/__tests__/decode-worker-source.test.ts +249 -0
  19. package/src/engine/__tests__/demux-ranged.test.ts +495 -0
  20. package/src/engine/__tests__/demux-truncated.test.ts +136 -0
  21. package/src/engine/__tests__/demux.test.ts +305 -0
  22. package/src/engine/__tests__/eligibility.test.ts +146 -0
  23. package/src/engine/__tests__/engine-recovery.test.ts +263 -0
  24. package/src/engine/__tests__/engine.test.ts +326 -0
  25. package/src/engine/__tests__/frame-server-ranged.test.ts +261 -0
  26. package/src/engine/__tests__/frame-server.test.ts +326 -0
  27. package/src/engine/__tests__/media-loader-ranged.test.ts +289 -0
  28. package/src/engine/__tests__/media-loader.test.ts +100 -0
  29. package/src/engine/__tests__/scheduler-crop.test.ts +353 -0
  30. package/src/engine/__tests__/scheduler.test.ts +1310 -0
  31. package/src/engine/__tests__/scrub-resolve.test.ts +96 -0
  32. package/src/engine/__tests__/scrub-source.test.ts +195 -0
  33. package/src/engine/__tests__/source-host.test.ts +455 -0
  34. package/src/engine/__tests__/time-stretch.test.ts +182 -0
  35. package/src/engine/audio-clock.ts +1179 -0
  36. package/src/engine/audio-worklet-source.ts +160 -0
  37. package/src/engine/batch-planner.ts +308 -0
  38. package/src/engine/debug-hud.tsx +54 -0
  39. package/src/engine/decode-worker-source.ts +142 -0
  40. package/src/engine/demux.ts +900 -0
  41. package/src/engine/eligibility.ts +140 -0
  42. package/src/engine/frame-server.ts +644 -0
  43. package/src/engine/index.ts +950 -0
  44. package/src/engine/media-loader.ts +380 -0
  45. package/src/engine/mp4box.d.ts +214 -0
  46. package/src/engine/scheduler.ts +1324 -0
  47. package/src/engine/scrub-resolve.ts +66 -0
  48. package/src/engine/scrub-source.ts +496 -0
  49. package/src/engine/time-stretch.ts +224 -0
  50. package/src/index.ts +83 -1
  51. package/src/preview/OverlayPreview.tsx +2 -24
  52. package/src/schema.ts +146 -3
  53. package/src/state/__tests__/use-project-sync.test.tsx +56 -0
  54. package/src/state/use-project-sync.ts +17 -0
  55. package/src/test-setup.ts +32 -0
  56. package/src/text/FontPicker.tsx +85 -51
  57. package/src/text/TextFormattingToolbar.tsx +6 -1
  58. package/src/text/__tests__/FontPicker.options.test.ts +43 -0
  59. package/src/text/__tests__/TextFormattingToolbar.test.tsx +4 -4
  60. package/src/theme.ts +108 -3
  61. package/src/types.ts +601 -22
  62. package/src/ui/Loader.tsx +64 -0
  63. package/src/ui/NumberField.tsx +254 -0
  64. package/src/ui/Slider.tsx +128 -0
  65. package/src/ui/Tooltip.tsx +118 -0
  66. package/src/ui/__tests__/NumberField.test.tsx +298 -0
  67. package/src/ui/__tests__/Slider.test.tsx +150 -0
  68. package/src/ui/__tests__/Tooltip.test.tsx +105 -0
  69. package/src/ui/__tests__/usePersistentState.test.tsx +112 -0
  70. package/src/ui/badge.tsx +12 -4
  71. package/src/ui/index.ts +6 -0
  72. package/src/ui/input.tsx +1 -1
  73. package/src/ui/select.tsx +1 -1
  74. package/src/ui/switch.tsx +12 -2
  75. package/src/ui/textarea.tsx +1 -1
  76. package/src/ui/usePersistentState.ts +63 -0
  77. package/src/video/AudioPolishModal.tsx +983 -0
  78. package/src/video/CaptionListPanel.test.tsx +944 -0
  79. package/src/video/CaptionListPanel.tsx +1106 -0
  80. package/src/video/CaptionRegenModal.tsx +37 -9
  81. package/src/video/CaptionSpecimen.tsx +160 -0
  82. package/src/video/CaptionStyleGallery.tsx +466 -0
  83. package/src/video/CommandPalette.tsx +165 -0
  84. package/src/video/ImageToneMenu.tsx +137 -0
  85. package/src/video/OverlayInspector.tsx +977 -0
  86. package/src/video/RenderModal.tsx +1035 -62
  87. package/src/video/VersionCompare.tsx +258 -0
  88. package/src/video/VersionPanel.tsx +117 -42
  89. package/src/video/VideoEditor.tsx +2229 -243
  90. package/src/video/__tests__/AudioPolishModal.test.tsx +825 -0
  91. package/src/video/__tests__/CaptionListPanel.font.test.tsx +397 -0
  92. package/src/video/__tests__/CaptionListPanel.generate.test.tsx +153 -0
  93. package/src/video/__tests__/CaptionRegenModal.test.tsx +29 -0
  94. package/src/video/__tests__/CaptionSpecimen.test.tsx +213 -0
  95. package/src/video/__tests__/CaptionStyleGallery.test.tsx +362 -0
  96. package/src/video/__tests__/CommandPalette.test.tsx +119 -0
  97. package/src/video/__tests__/OverlayInspector.test.tsx +1367 -0
  98. package/src/video/__tests__/RenderModal.exportControls.test.tsx +319 -0
  99. package/src/video/__tests__/RenderModal.options.test.tsx +562 -0
  100. package/src/video/__tests__/RenderModal.progress.test.ts +65 -0
  101. package/src/video/__tests__/VersionCompare.test.tsx +182 -0
  102. package/src/video/__tests__/VersionPanel.test.tsx +279 -0
  103. package/src/video/__tests__/VideoEditor.audioPolish.test.tsx +241 -0
  104. package/src/video/__tests__/VideoEditor.captionDelete.test.tsx +147 -0
  105. package/src/video/__tests__/VideoEditor.captionGesture.test.tsx +170 -0
  106. package/src/video/__tests__/VideoEditor.captionSeam.test.tsx +134 -0
  107. package/src/video/__tests__/VideoEditor.clipKeyframes.test.tsx +59 -0
  108. package/src/video/__tests__/VideoEditor.context.test.tsx +44 -0
  109. package/src/video/__tests__/VideoEditor.editFocus.test.tsx +55 -0
  110. package/src/video/__tests__/VideoEditor.keymap.test.tsx +725 -0
  111. package/src/video/__tests__/VideoEditor.layout.test.tsx +338 -0
  112. package/src/video/__tests__/VideoEditor.propertiesPanel.test.tsx +765 -0
  113. package/src/video/__tests__/VideoEditor.rippleDeleteCaptions.test.tsx +159 -0
  114. package/src/video/__tests__/VideoEditor.sourcePreview.test.tsx +122 -0
  115. package/src/video/__tests__/VideoEditor.test.tsx +403 -50
  116. package/src/video/__tests__/audioMagnet.test.ts +158 -0
  117. package/src/video/__tests__/audioPolish.test.ts +1202 -0
  118. package/src/video/__tests__/captionActiveWord.test.ts +107 -0
  119. package/src/video/__tests__/captionLanes.test.ts +262 -0
  120. package/src/video/__tests__/captionPositioning.test.tsx +8 -3
  121. package/src/video/__tests__/captionWordFloor.test.ts +228 -0
  122. package/src/video/__tests__/clipboard-ops.test.ts +430 -0
  123. package/src/video/__tests__/cuts.insert.test.ts +244 -0
  124. package/src/video/__tests__/cuts.test.ts +1213 -34
  125. package/src/video/__tests__/export-limits.test.ts +195 -0
  126. package/src/video/__tests__/hover-scrub.test.ts +163 -0
  127. package/src/video/__tests__/keyframeOps.canKeyframeProp.test.ts +61 -0
  128. package/src/video/__tests__/keyframeOps.test.ts +643 -0
  129. package/src/video/__tests__/keymap.test.tsx +171 -0
  130. package/src/video/__tests__/render-progress.test.tsx +20 -4
  131. package/src/video/__tests__/shuttle.test.ts +210 -0
  132. package/src/video/__tests__/source-preview.test.ts +60 -0
  133. package/src/video/__tests__/timecode.test.ts +77 -0
  134. package/src/video/__tests__/use-report-context.test.tsx +101 -0
  135. package/src/video/audioMagnet.ts +72 -0
  136. package/src/video/audioPolish.ts +774 -0
  137. package/src/video/captionActiveWord.ts +74 -0
  138. package/src/video/captionLanes.ts +202 -0
  139. package/src/video/captionRepair.ts +9 -5
  140. package/src/video/captionStyleDefaults.ts +100 -0
  141. package/src/video/captionWordFloor.ts +83 -0
  142. package/src/video/clipboard-ops.ts +377 -0
  143. package/src/video/cuts.ts +713 -37
  144. package/src/video/export-limits.ts +102 -0
  145. package/src/video/hover-scrub.ts +102 -0
  146. package/src/video/imageTone.ts +58 -0
  147. package/src/video/imageToneExamples.ts +10 -0
  148. package/src/video/keyframeOps.ts +384 -0
  149. package/src/video/keymap.ts +146 -0
  150. package/src/video/panels/ClipPropertiesPanel.tsx +706 -0
  151. package/src/video/panels/LeftPanelTabs.tsx +187 -0
  152. package/src/video/panels/OverlayContentPanel.tsx +351 -0
  153. package/src/video/panels/TabNav.tsx +60 -0
  154. package/src/video/panels/__tests__/ClipPropertiesPanel.test.tsx +645 -0
  155. package/src/video/panels/__tests__/LeftPanelTabs.test.tsx +189 -0
  156. package/src/video/panels/__tests__/OverlayContentPanel.test.tsx +222 -0
  157. package/src/video/panels/__tests__/TabNav.test.tsx +71 -0
  158. package/src/video/preview/CaptionPreview.tsx +57 -69
  159. package/src/video/preview/EngineSurface.tsx +103 -0
  160. package/src/video/preview/OverlayItemsLayer.tsx +315 -103
  161. package/src/video/preview/PreviewPlayer.tsx +337 -56
  162. package/src/video/preview/SocialPreviewMenu.tsx +214 -0
  163. package/src/video/preview/SocialSafeZoneOverlay.tsx +478 -0
  164. package/src/video/preview/__tests__/CaptionPreview.fonts.test.tsx +97 -0
  165. package/src/video/preview/__tests__/EngineSurface.test.tsx +114 -0
  166. package/src/video/preview/__tests__/OverlayItemsLayer.edit.test.tsx +4 -2
  167. package/src/video/preview/__tests__/OverlayItemsLayer.keyframes.test.tsx +363 -0
  168. package/src/video/preview/__tests__/OverlayItemsLayer.selection.test.tsx +329 -0
  169. package/src/video/preview/__tests__/PreviewPlayer.engine.test.tsx +139 -0
  170. package/src/video/preview/__tests__/SocialPreviewMenu.test.tsx +121 -0
  171. package/src/video/preview/__tests__/SocialSafeZoneOverlay.test.tsx +165 -0
  172. package/src/video/preview/__tests__/captionDragState.test.ts +120 -1
  173. package/src/video/preview/__tests__/latencyCompensation.test.tsx +451 -0
  174. package/src/video/preview/__tests__/proxySupport.test.ts +75 -0
  175. package/src/video/preview/__tests__/transformStyle.test.ts +29 -1
  176. package/src/video/preview/__tests__/useDragOverlay.perAxis.test.ts +197 -0
  177. package/src/video/preview/__tests__/useEnginePlayback.test.tsx +530 -0
  178. package/src/video/preview/__tests__/useVideoPlayback.corpus.test.ts +313 -0
  179. package/src/video/preview/__tests__/useVideoPlayback.test.ts +38 -5
  180. package/src/video/preview/__tests__/useVideoPlayback.trackAudio.test.ts +278 -0
  181. package/src/video/preview/audio-context.ts +111 -0
  182. package/src/video/preview/captionDragState.ts +91 -1
  183. package/src/video/preview/proxySupport.ts +86 -0
  184. package/src/video/preview/transformStyle.ts +22 -12
  185. package/src/video/preview/useDragOverlay.ts +92 -18
  186. package/src/video/preview/useEnginePlayback.ts +625 -0
  187. package/src/video/preview/useVideoPlayback.ts +211 -167
  188. package/src/video/sdrCurves.ts +56 -0
  189. package/src/video/shuttle.ts +159 -0
  190. package/src/video/source-preview.ts +66 -0
  191. package/src/video/timecode.ts +59 -0
  192. package/src/video/timeline/EditableSegment.tsx +1 -1
  193. package/src/video/timeline/Scrubber.tsx +46 -174
  194. package/src/video/timeline/SpeedControl.tsx +95 -0
  195. package/src/video/timeline/Timeline.tsx +1061 -328
  196. package/src/video/timeline/TimelineContext.ts +17 -10
  197. package/src/video/timeline/TrackGutter.tsx +560 -0
  198. package/src/video/timeline/TrackSettingsPopover.tsx +228 -0
  199. package/src/video/timeline/VolumeControl.tsx +113 -0
  200. package/src/video/timeline/__tests__/Timeline.backgroundClick.test.tsx +110 -0
  201. package/src/video/timeline/__tests__/Timeline.crossfade.test.tsx +104 -0
  202. package/src/video/timeline/__tests__/Timeline.fadeCurveMenu.test.tsx +174 -0
  203. package/src/video/timeline/__tests__/Timeline.keyframeDelete.test.tsx +273 -0
  204. package/src/video/timeline/__tests__/Timeline.keyframeFollow.test.tsx +215 -0
  205. package/src/video/timeline/__tests__/Timeline.keyframeMenu.test.tsx +253 -0
  206. package/src/video/timeline/__tests__/Timeline.keymap.test.tsx +372 -0
  207. package/src/video/timeline/__tests__/Timeline.subcutRegen.test.tsx +351 -0
  208. package/src/video/timeline/__tests__/TrackGutter.test.tsx +372 -0
  209. package/src/video/timeline/__tests__/_canvasSelect.test.tsx +273 -0
  210. package/src/video/timeline/__tests__/_canvasSelect.ts +414 -0
  211. package/src/video/timeline/__tests__/dragdrop-math.test.ts +135 -0
  212. package/src/video/timeline/__tests__/effectiveItemAudio.test.ts +50 -0
  213. package/src/video/timeline/__tests__/enabledTrackItems.test.ts +164 -0
  214. package/src/video/timeline/__tests__/moveItemAcrossTracks.test.ts +363 -0
  215. package/src/video/timeline/__tests__/multiSelectOps.test.ts +447 -0
  216. package/src/video/timeline/__tests__/placement.test.ts +278 -0
  217. package/src/video/timeline/__tests__/resizeWindowedItem.test.ts +140 -0
  218. package/src/video/timeline/__tests__/timeline-model.test.ts +576 -0
  219. package/src/video/timeline/__tests__/visualItemLabel.test.ts +69 -0
  220. package/src/video/timeline/canvas/TimelineCanvas.tsx +1447 -0
  221. package/src/video/timeline/canvas/__tests__/TimelineCanvas.drop.test.tsx +439 -0
  222. package/src/video/timeline/canvas/__tests__/TimelineCanvas.edgeScroll.test.tsx +346 -0
  223. package/src/video/timeline/canvas/__tests__/TimelineCanvas.panefill.test.tsx +122 -0
  224. package/src/video/timeline/canvas/__tests__/TimelineCanvas.pendingDrops.test.tsx +316 -0
  225. package/src/video/timeline/canvas/__tests__/TimelineCanvas.pointer.test.tsx +606 -0
  226. package/src/video/timeline/canvas/__tests__/TimelineCanvas.test.tsx +407 -0
  227. package/src/video/timeline/canvas/__tests__/clip-bands.test.ts +77 -0
  228. package/src/video/timeline/canvas/__tests__/draw.test.ts +2198 -0
  229. package/src/video/timeline/canvas/__tests__/fade-curve.test.ts +187 -0
  230. package/src/video/timeline/canvas/__tests__/filmstrips.test.ts +561 -0
  231. package/src/video/timeline/canvas/__tests__/hit-test.test.ts +818 -0
  232. package/src/video/timeline/canvas/__tests__/pending-drop.test.ts +210 -0
  233. package/src/video/timeline/canvas/__tests__/pointer-machine.test.ts +3358 -0
  234. package/src/video/timeline/canvas/__tests__/snap.test.ts +257 -0
  235. package/src/video/timeline/canvas/__tests__/viewport.test.ts +399 -0
  236. package/src/video/timeline/canvas/__tests__/waveforms.test.ts +946 -0
  237. package/src/video/timeline/canvas/clip-bands.ts +56 -0
  238. package/src/video/timeline/canvas/draw.ts +2187 -0
  239. package/src/video/timeline/canvas/fade-curve.ts +111 -0
  240. package/src/video/timeline/canvas/filmstrips.ts +418 -0
  241. package/src/video/timeline/canvas/hit-test.ts +501 -0
  242. package/src/video/timeline/canvas/keyframe-strip.ts +73 -0
  243. package/src/video/timeline/canvas/pointer-machine.ts +1828 -0
  244. package/src/video/timeline/canvas/snap.ts +232 -0
  245. package/src/video/timeline/canvas/viewport.ts +457 -0
  246. package/src/video/timeline/canvas/waveforms.ts +664 -0
  247. package/src/video/timeline/makeCaptionEdit.ts +5 -1
  248. package/src/video/timeline/multiSelectOps.ts +214 -55
  249. package/src/video/timeline/placement.ts +282 -0
  250. package/src/video/timeline/timeline-model.ts +919 -0
  251. package/src/video/timeline/useItemDragDrop.ts +151 -178
  252. package/src/video/timeline/useTimelineZoom.ts +25 -60
  253. package/src/video/timeline/utils.ts +0 -12
  254. package/src/video/use-report-context.ts +75 -0
  255. package/src/video/preview/OverlayPropsModal.tsx +0 -292
  256. package/src/video/preview/__tests__/OverlayPropsModal.test.tsx +0 -32
  257. package/src/video/timeline/AudioTrackRow.tsx +0 -404
  258. package/src/video/timeline/AudioWaveformLayer.tsx +0 -117
  259. package/src/video/timeline/CaptionTrackRow.tsx +0 -235
  260. package/src/video/timeline/PlayheadLine.tsx +0 -18
  261. package/src/video/timeline/TranscriptModal.tsx +0 -70
  262. package/src/video/timeline/TranscriptPanel.tsx +0 -273
  263. package/src/video/timeline/VisualTrackRow.tsx +0 -300
  264. package/src/video/timeline/__tests__/CaptionTrackRow.test.tsx +0 -241
  265. package/src/video/timeline/__tests__/PlayheadLine.test.tsx +0 -60
  266. package/src/video/timeline/__tests__/TranscriptModal.test.tsx +0 -41
  267. package/src/video/timeline/__tests__/TranscriptPanel.test.tsx +0 -184
  268. package/src/video/timeline/__tests__/useItemDragDrop.test.ts +0 -72
package/src/types.ts CHANGED
@@ -23,6 +23,8 @@ import type {
23
23
  OverlayElement,
24
24
  Captions,
25
25
  } from './schema'
26
+ import type { ImageTone } from './video/imageTone'
27
+ import type { SourcePreviewStore } from './video/source-preview'
26
28
 
27
29
  // ── Overlay compiler ─────────────────────────────────────────────────────────
28
30
 
@@ -55,25 +57,86 @@ export type { Project, Slide, CarouselElement, ImageElement, OverlayElement }
55
57
  */
56
58
  export type RenderEvent =
57
59
  | { type: 'log'; message: string }
58
- | { type: 'done'; outputPath: string }
60
+ /**
61
+ * `outputPath` is always the primary (master) file. `outputPaths` is present
62
+ * only when the render emitted more than one file (an `--export both` HDR
63
+ * render: master first, derived SDR sibling second) AND the host was able to
64
+ * learn the full list — hosts that can't simply omit it.
65
+ */
66
+ | { type: 'done'; outputPath: string; outputPaths?: string[] }
59
67
  | { type: 'error'; message: string }
60
68
 
61
69
  /**
62
- * Options for a render request. Kept intentionally minimal — Montaj's render
63
- * endpoint (`POST /api/projects/:id/render`) takes no body today, so `scale`
64
- * is the only forward-looking knob and is optional. Hosts ignore fields they
65
- * don't support.
70
+ * Which file(s) a render produces for an HDR project:
71
+ * - 'auto' — one master in the project's own color space (an HDR project stays
72
+ * HDR). The historical behavior and the default.
73
+ * - 'sdr' — one standard-range file, tone-mapped through `sdrCurve`.
74
+ * - 'both' — the HDR master plus an SDR sibling derived from it.
75
+ * SDR projects are unaffected: every value renders the same single SDR file.
76
+ */
77
+ export type RenderExport = 'auto' | 'sdr' | 'both'
78
+
79
+ /**
80
+ * Options for a render request. Hosts ignore fields they don't support, and
81
+ * omitting the whole object keeps a host's default behavior.
66
82
  */
67
83
  export interface RenderOptions {
68
84
  /** Output scale multiplier (1 = native resolution). */
69
85
  scale?: number
86
+ /** Which file(s) an HDR project exports. Defaults to 'auto' host-side. */
87
+ export?: RenderExport
88
+ /**
89
+ * Id of the HDR→SDR tone curve used for any SDR output (see
90
+ * `video/sdrCurves.ts` for the descriptors, and the `curves` keys in
91
+ * montaj_assets/luts/looks.json for what a host validates against). Typed as
92
+ * `string` so a host can ship curves the package doesn't know about; ignored
93
+ * when the render produces no SDR file.
94
+ */
95
+ sdrCurve?: string
96
+ /**
97
+ * Output base filename (no extension) for the rendered file, from the export
98
+ * dialog's Name field. Hosts that support naming the output read this; others
99
+ * ignore it. Omitted → the host's default naming applies.
100
+ */
101
+ name?: string
102
+ /**
103
+ * Poster/cover frame timecode in project-timeline seconds, from the export
104
+ * dialog's cover picker. Hosts that produce a cover read this; others ignore
105
+ * it. Omitted → the host's default (e.g. first frame) applies.
106
+ */
107
+ cover?: number
108
+ }
109
+
110
+ /**
111
+ * Options for a sample-frame request. Only the SDR curve for now — the frame
112
+ * itself is identified by the project id and timestamp.
113
+ */
114
+ export interface SampleFrameOptions {
115
+ /** Tone curve to map an HDR project's frame through (see `RenderOptions.sdrCurve`). */
116
+ sdrCurve?: string
117
+ /**
118
+ * Prefer each clip's SDR proxy over the full-resolution master for a fast
119
+ * preview (cover posters, the cover-frame grid). The proxy is already SDR, so
120
+ * this is mutually exclusive with `sdrCurve` — a proxy frame cannot show a
121
+ * per-curve HDR→SDR grade. Adapters that can't sample from a proxy may ignore
122
+ * it. Falls back to the master per clip when no proxy exists.
123
+ */
124
+ preferProxy?: boolean
70
125
  }
71
126
 
72
127
  /**
73
128
  * Coarse phase of an async render pipeline. Ordered roughly by execution order;
74
- * hosts may skip phases that don't apply to their pipeline.
129
+ * hosts may skip phases that don't apply to their pipeline — `sdr_derive` in
130
+ * particular only runs when an HDR project exports SDR (`export: 'sdr' | 'both'`).
75
131
  */
76
- export type RenderPhase = 'preparing' | 'rendering' | 'captions' | 'encoding' | 'saving' | 'done'
132
+ export type RenderPhase =
133
+ | 'preparing'
134
+ | 'rendering'
135
+ | 'captions'
136
+ | 'encoding'
137
+ | 'sdr_derive'
138
+ | 'saving'
139
+ | 'done'
77
140
 
78
141
  /**
79
142
  * Point-in-time snapshot of an async render's progress. Returned by
@@ -168,6 +231,19 @@ export interface VersionEntry {
168
231
  timestamp: string
169
232
  }
170
233
 
234
+ // ── Host path resolution ─────────────────────────────────────────────────────
235
+
236
+ /**
237
+ * Resolves a host-internal asset path into a displayable URL (e.g. Montaj's
238
+ * `/api/files?path=…`). This is `EditorAdapter.fileUrl`'s signature, named so
239
+ * the components it is threaded down to as a prop can type it without
240
+ * depending on the whole adapter: the canvas timeline takes it to display the
241
+ * `path`s that come back on `WaveformChunk` and `FilmstripSheet` below.
242
+ * Optional wherever it is a prop — a host that omits it gets identity, and the
243
+ * feature that needed the URL degrades to nothing rather than erroring.
244
+ */
245
+ export type ResolveFilePath = (path: string) => string
246
+
171
247
  // ── Waveform chunks (optional capability) ─────────────────────────────────────
172
248
 
173
249
  /**
@@ -182,6 +258,156 @@ export interface WaveformChunk {
182
258
  end: number
183
259
  }
184
260
 
261
+ // ── Waveform peaks & filmstrips (optional capability) ─────────────────────────
262
+
263
+ /**
264
+ * Zoom-bucketed audio peak data for a scrubbable waveform view — the
265
+ * canvas-rendered replacement for `WaveformChunk`'s fixed PNGs. `peaks` is
266
+ * interleaved `[min, max, min, max, ...]`, one pair per sample bucket, values
267
+ * in int16 range. `samplesPerSecond` is normally one of the requested
268
+ * resolution buckets (50 | 200 | 800) but may come back as a lower,
269
+ * non-bucketed number when the host's total-samples clamp forced a step-down
270
+ * for a long window — the actual resolution used, never silently truncated,
271
+ * hence `number` rather than a literal union. `start`/`duration` echo the
272
+ * decoded source-time window in seconds. Maps to Montaj's `waveform_peaks`
273
+ * step.
274
+ */
275
+ export interface PeaksData {
276
+ samplesPerSecond: number
277
+ start: number
278
+ duration: number
279
+ peaks: number[]
280
+ }
281
+
282
+ /** The three bucketed resolutions `getWaveformPeaks` may request. */
283
+ export type PeaksResolution = 50 | 200 | 800
284
+
285
+ /**
286
+ * Args for `EditorAdapter.getWaveformPeaks`. `projectId` scopes the host's
287
+ * output cache (mirrors `getWaveformChunks`'s explicit `projectId`, since a
288
+ * single adapter instance isn't itself project-scoped); `src` is the
289
+ * source-identity path — a proxy or original file, per the caller's
290
+ * input-selection policy (see the Montaj adapter implementation comment);
291
+ * `samplesPerSecond` is the requested resolution bucket; `start`/`duration`
292
+ * optionally window the request to part of the source (omit for the whole
293
+ * file).
294
+ */
295
+ export interface GetWaveformPeaksArgs {
296
+ projectId: string
297
+ src: string
298
+ samplesPerSecond: PeaksResolution
299
+ start?: number
300
+ duration?: number
301
+ }
302
+
303
+ /**
304
+ * One tiled contact sheet in a `FilmstripIndex`. `path` is a host-resolvable
305
+ * image path (route through `fileUrl` to display — same convention as
306
+ * `WaveformChunk.path`). `tiles` maps each cell to its source timestamp `t`
307
+ * (seconds) and its `row`/`col` position in the sheet's `cols` x `rows` grid.
308
+ */
309
+ export interface FilmstripSheet {
310
+ path: string
311
+ cols: number
312
+ rows: number
313
+ tiles: Array<{ t: number; row: number; col: number }>
314
+ }
315
+
316
+ /**
317
+ * A video's uniform time-grid thumbnail strip, tiled across one or more
318
+ * `FilmstripSheet`s. `interval` is the seconds between consecutive tiles
319
+ * (uniform across the whole filmstrip); `tileWidth` is the pixel width every
320
+ * tile was scaled to. Maps to Montaj's `filmstrip` step.
321
+ */
322
+ export interface FilmstripIndex {
323
+ sheets: FilmstripSheet[]
324
+ interval: number
325
+ tileWidth: number
326
+ }
327
+
328
+ /**
329
+ * Args for `EditorAdapter.getFilmstrip`. `projectId` scopes the host's output
330
+ * cache; `src` is the source-identity path (proxy-only, per the video
331
+ * timeline's filmstrip policy — see the Montaj adapter implementation
332
+ * comment). The grid params mirror the `filmstrip` step's own knobs and are
333
+ * optional — omit to use the step's defaults (`max-tiles=100`,
334
+ * `min-interval=1.0`, `tile-width=160`).
335
+ */
336
+ export interface GetFilmstripArgs {
337
+ projectId: string
338
+ src: string
339
+ maxTiles?: number
340
+ minInterval?: number
341
+ tileWidth?: number
342
+ }
343
+
344
+ // ── Audio polish (optional capability) ────────────────────────────────────────
345
+
346
+ /**
347
+ * Args for `EditorAdapter.analyzeAudioPolish`. `projectId` scopes the host's
348
+ * job/output cache (mirrors `getWaveformPeaks`/`getFilmstrip`); `piece`
349
+ * selects which of Montaj's four audio-polish steps to run (or the
350
+ * `'silence-check'` dry run — see `AudioPolishAnalysis`); `src` is the
351
+ * source-identity path being analyzed. `window`, when given, restricts
352
+ * analysis to a slice of the source — in **source** seconds, not timeline
353
+ * seconds (see `AudioPolishAnalysis` for why that distinction matters).
354
+ * `options` are piece-specific knobs: `language`/`model` steer `fillers`'
355
+ * speech recognition, `targetLufs` steers `loudness`'s gain calculation, and
356
+ * `maxWordGap`/`sentenceEdge` steer `silence`/`silence-check`'s gap-merging
357
+ * heuristics.
358
+ */
359
+ export interface AnalyzeAudioPolishArgs {
360
+ projectId: string
361
+ piece: 'silence' | 'fillers' | 'loudness' | 'voice' | 'silence-check'
362
+ src: string
363
+ window?: { in: number; out: number }
364
+ options?: {
365
+ /**
366
+ * Speech-recognition language hint for `fillers` (e.g. `'en'`). Defaults
367
+ * to `'en'` at the call site and should be surfaced as a visible user
368
+ * control — a wrong language silently corrupts detection rather than
369
+ * failing loudly.
370
+ */
371
+ language?: string
372
+ model?: string
373
+ targetLufs?: number
374
+ maxWordGap?: number
375
+ sentenceEdge?: number
376
+ }
377
+ }
378
+
379
+ /**
380
+ * Result of `EditorAdapter.analyzeAudioPolish`, discriminated on `piece`.
381
+ * **Every time in here is source time** — an offset into the source file
382
+ * named by the request's `src`, never timeline time. Mixing the two is the
383
+ * single easiest way to misuse this type; a caller must convert through the
384
+ * clip's own timeline↔source mapping before applying a removal/keep to the
385
+ * project.
386
+ *
387
+ * - `'silence'` / `'fillers'` — `removals`, each a source-time span to cut,
388
+ * with optional `text` (the recognized words, for `fillers`).
389
+ * - `'silence-check'` — `keeps`, the source-time spans that WOULD survive
390
+ * silence removal at the current settings — the inverse framing of
391
+ * `'silence'`, for previewing a threshold before committing to it.
392
+ * - `'loudness'` — the measured integrated/true-peak/LRA loudness, the
393
+ * requested target, and the gain in dB needed to reach it.
394
+ * - `'voice'` — the isolated vocal track: `vocalsPath` is the host path,
395
+ * `url` a directly displayable/fetchable URL for it (same convention as
396
+ * `getSampleFrame`'s `url`).
397
+ */
398
+ export type AudioPolishAnalysis =
399
+ | { piece: 'silence' | 'fillers'; removals: Array<{ start: number; end: number; text?: string }> }
400
+ | { piece: 'silence-check'; keeps: Array<[number, number]> }
401
+ | {
402
+ piece: 'loudness'
403
+ measuredI: number
404
+ measuredTP: number
405
+ measuredLRA: number
406
+ targetI: number
407
+ gainDb: number
408
+ }
409
+ | { piece: 'voice'; vocalsPath: string; url: string }
410
+
185
411
  // ── Media (optional capability) ───────────────────────────────────────────────
186
412
 
187
413
  /**
@@ -206,8 +432,93 @@ export interface MediaItem {
206
432
  name?: string
207
433
  }
208
434
 
435
+ // ── Footage bin drag-and-drop (optional capability) ───────────────────────────
436
+
437
+ /**
438
+ * The drag payload for dropping a bin clip onto the timeline. A subset of
439
+ * `VisualItem`'s fields — just enough for the timeline drop target to insert a
440
+ * new clip without round-tripping through the host.
441
+ */
442
+ export interface FootageDropPayload {
443
+ src: string
444
+ proxySrc?: string
445
+ sourceDuration: number
446
+ sourceWidth?: number
447
+ sourceHeight?: number
448
+ name?: string
449
+ }
450
+
451
+ /**
452
+ * The custom drag-and-drop MIME type carrying a `FootageDropPayload` JSON
453
+ * string from the footage bin to the timeline drop target.
454
+ */
455
+ export const FOOTAGE_DND_MIME = 'application/x-montaj-footage'
456
+
457
+ // ── Filesystem drops onto the timeline (optional capability) ──────────────────
458
+ // The SECOND way footage reaches the timeline by drag: real files from the OS,
459
+ // dropped straight onto the track surface. Unlike a bin drag (above) the
460
+ // package cannot place these itself — a `File` has no duration, no proxy and no
461
+ // host-resolvable path until the host has probed and ingested it — so the
462
+ // package's half is only "where did they drop it, and what did they drop";
463
+ // everything after that is the host's job, and it reports progress back as the
464
+ // `PendingDrop` ghosts below.
465
+
466
+ /** What a filesystem drop reports about ITSELF: where the pointer released on
467
+ * the timeline, which row it released over, and the ripple/magnet mode
468
+ * captured at that instant. Named once here — `onImportFilesToTimeline`
469
+ * below, `Timeline.tsx`, and `TimelineCanvas.tsx` all threaded the same
470
+ * shape as an inline object literal, three chances for it to drift — and
471
+ * re-exported from `index.ts` so a host (`timelineImport.ts`) can name it
472
+ * too instead of keeping its own duplicate. */
473
+ export interface TimelineDropPlacement {
474
+ atTime: number
475
+ preferredTrackIndex: number
476
+ ripple: boolean
477
+ }
478
+
479
+ /** A filesystem file mid-import from a timeline drop, drawn as a ghost band at
480
+ * the drop point until the host's import resolves. */
481
+ export interface PendingDrop {
482
+ /** Host-owned id, so the host can retract this ghost when its import
483
+ * resolves or fails. */
484
+ id: string
485
+ /** Where the ghost band starts, in timeline seconds. */
486
+ atTime: number
487
+ /** The band's length in seconds — the host's fast local probe of the file. */
488
+ durationSec: number
489
+ /** Which video row it is drawn on, in normalized track order. -1 → the base
490
+ * video row. */
491
+ trackIndex: number
492
+ /** The clip will land on a NEW video track (no existing row matches
493
+ * `trackIndex` — it names a row that does not exist yet, per
494
+ * `resolveDropTrackIndex`'s own doc). Draw the ghost on a FRESH row where
495
+ * that track will appear once ingest finishes and `placeDroppedClip`
496
+ * actually creates it, not on the base video row: the base row may already
497
+ * carry real footage, and a ghost sitting on top of it reads as
498
+ * overlapping a clip that has nothing to do with this drop. Absent/false →
499
+ * `trackIndex` names (or falls back to) an existing row, drawn exactly as
500
+ * before. */
501
+ newTrack?: boolean
502
+ /** Filename, drawn inside the band. */
503
+ label?: string
504
+ }
505
+
209
506
  // ── Adapter ────────────────────────────────────────────────────────────────
210
507
 
508
+ /**
509
+ * What the editor is looking at right now — ephemeral UI state, never part of
510
+ * the project document. Reported to the host so an agent can resolve "this
511
+ * section" against the actual playhead instead of guessing.
512
+ */
513
+ export interface EditorContext {
514
+ /** Playhead position in project seconds. */
515
+ playheadSec: number
516
+ /** All selected timeline item ids; [0] is the primary. */
517
+ selectedIds: string[]
518
+ /** Selected caption segment id, if any. */
519
+ selectedCaptionId: string | null
520
+ }
521
+
211
522
  /**
212
523
  * The contract a host implements to drive the editor. All transport,
213
524
  * authentication, and URL-shape concerns live behind this interface; the
@@ -270,6 +581,19 @@ export interface EditorAdapter<P extends Project = Project> {
270
581
  */
271
582
  listMedia?(scope: MediaScope): Promise<MediaItem[]>
272
583
 
584
+ /**
585
+ * Optional: kick a background ingest of a new source clip (probe →
586
+ * normalize to the project's color space → proxy → register in
587
+ * `project.sources`). `input` is either a host-resolvable path already on
588
+ * the host's filesystem, or a `File` the adapter uploads itself. Resolves
589
+ * with a job id the caller can poll. Optional — hosts that don't support
590
+ * post-init ingest omit it.
591
+ */
592
+ ingestSource?(
593
+ projectId: string,
594
+ input: { path: string } | File,
595
+ ): Promise<{ jobId: string }>
596
+
273
597
  /**
274
598
  * Compile a JSX overlay template file into an `OverlayFactory`.
275
599
  * The host supplies this because the compilation pipeline (Babel, fetch
@@ -362,11 +686,31 @@ export interface EditorAdapter<P extends Project = Project> {
362
686
  restoreVersion?(id: string, hash: string): Promise<P>
363
687
 
364
688
  /**
365
- * Optional: produce rendered waveform-image chunks for an audio track. The
366
- * editor passes the project id, the track id (used to namespace the output
367
- * cache), the track's source path, and an optional chunk duration in seconds.
368
- * The host renders/caches the chunks and returns their resolvable paths. Maps
369
- * to Montaj's `waveform_image` step.
689
+ * Optional: save the current project state as a named version. Maps to
690
+ * Montaj's `POST /api/projects/:id/versions` with `{ name? }`. Returns the
691
+ * updated version list.
692
+ */
693
+ saveVersion?(id: string, name?: string): Promise<VersionEntry[]>
694
+
695
+ /**
696
+ * Optional: build the URL for a rendered frame from a specific version
697
+ * (git commit hash, or the string `"working"` for the live on-disk state)
698
+ * at time `t` seconds. The URL is used as an `<img src>`; the host serves
699
+ * the PNG. Maps to Montaj's `GET /api/projects/:id/versions/:commit/frame?t=`.
700
+ */
701
+ versionFrameUrl?(id: string, commit: string, t: number): string
702
+
703
+ /**
704
+ * RETIRED — the editor no longer calls this. It produced rendered
705
+ * waveform-image chunks (fixed PNG strips) for the DOM timeline's audio
706
+ * rows; those rows are gone and the canvas timeline draws from
707
+ * `getWaveformPeaks` instead, which is now the package's only waveform
708
+ * path. The signature is kept so hosts that still implement it keep
709
+ * compiling, and so a host can go on serving it to its own chrome — but
710
+ * nothing in this package reads the result. Mapped to Montaj's
711
+ * `waveform_image` step; args were the project id, the track id (which
712
+ * namespaced the output cache), the track's source path, and an optional
713
+ * chunk duration in seconds.
370
714
  */
371
715
  getWaveformChunks?(
372
716
  projectId: string,
@@ -375,6 +719,48 @@ export interface EditorAdapter<P extends Project = Project> {
375
719
  chunkDurationS?: number,
376
720
  ): Promise<WaveformChunk[]>
377
721
 
722
+ /**
723
+ * Optional: produce zoom-bucketed audio peak data for a scrubbable
724
+ * waveform view. This is the package's ONLY waveform path — the canvas
725
+ * timeline draws every waveform from it (see the retired
726
+ * `getWaveformChunks` above). Input-selection policy is the *caller's*
727
+ * responsibility, not this method's: `item.proxySrc` (proxy only — no
728
+ * fallback to the original) for per-clip waveforms on visual tracks,
729
+ * `track.src` for audio lanes (see the Montaj adapter implementation
730
+ * comment). Maps to Montaj's
731
+ * `waveform_peaks` step. Optional: a host without a peaks step omits it and
732
+ * the editor feature-detects its absence, drawing no waveforms.
733
+ */
734
+ getWaveformPeaks?(args: GetWaveformPeaksArgs): Promise<PeaksData>
735
+
736
+ /**
737
+ * Optional: produce a uniform time-grid filmstrip (thumbnail strip) for a
738
+ * video source, tiled into one or more contact sheets with a timestamp
739
+ * index. Maps to Montaj's `filmstrip` step. Optional: a host without a
740
+ * filmstrip step omits it and the editor feature-detects its absence,
741
+ * drawing no tile strips or hover-scrub thumbs.
742
+ */
743
+ getFilmstrip?(args: GetFilmstripArgs): Promise<FilmstripIndex>
744
+
745
+ /**
746
+ * Optional: render one fully composited project frame at `at` (project
747
+ * timeline seconds) and return a directly displayable URL for it — a still
748
+ * the editor can put straight into an `<img>`. `opts.sdrCurve` picks the
749
+ * HDR→SDR tone curve so the same frame can be sampled through each curve for
750
+ * a side-by-side comparison (the RenderModal's curve picker).
751
+ *
752
+ * URL rather than a host path because the resolution rule is the host's:
753
+ * Montaj returns its `/api/files?path=` URL for the produced PNG, a Hub
754
+ * client would return a presigned one. Hosts without a frame sampler omit
755
+ * this; the editor feature-detects its absence and shows the picker without
756
+ * thumbnails. Maps to Montaj's `sample_frame` step.
757
+ */
758
+ getSampleFrame?(
759
+ projectId: string,
760
+ at: number,
761
+ opts?: SampleFrameOptions,
762
+ ): Promise<{ url: string }>
763
+
378
764
  /**
379
765
  * Optional: invalidate the host's compiled-overlay cache. When `src` is given,
380
766
  * only that entry is dropped; hosts may treat a missing `src` as a no-op or a
@@ -399,8 +785,49 @@ export interface EditorAdapter<P extends Project = Project> {
399
785
  * the editor patches `project.captions` from the 'done' event. Hosts without a
400
786
  * transcription pipeline omit this; the editor feature-detects its absence and
401
787
  * hides the "Regenerate captions" control.
788
+ *
789
+ * The 'done' `Captions` REPLACES `project.captions` wholesale, not just its
790
+ * segments in row 0 — a project with more than one caption row (see the
791
+ * `timeline` prop doc above) loses every row but the single fresh one this
792
+ * produces. `CaptionRegenModal` warns before that happens whenever the
793
+ * project has more than one row; there is no partial/per-row regeneration.
402
794
  */
403
795
  generateCaptions?(id: string, opts?: GenerateCaptionsOptions): AsyncIterable<CaptionEvent>
796
+
797
+ /**
798
+ * Optional: report the editor's live playhead and selection to the host.
799
+ *
800
+ * Fire-and-forget and already throttled by the editor (see
801
+ * `useReportContext`) — a host must not add its own debounce. Hosts with
802
+ * nowhere to put ephemeral UI state omit this entirely; the editor feature-
803
+ * detects its absence and reports nothing. A rejected promise is swallowed:
804
+ * context sync is a convenience and must never surface as an editor error.
805
+ */
806
+ reportContext?(id: string, context: EditorContext): Promise<void>
807
+
808
+ /**
809
+ * Optional: analyze a clip's audio for one of four cleanup pieces (or a
810
+ * `'silence-check'` dry run) and return proposed edits for the user to
811
+ * review before applying — detect silence/filler words to trim, measure
812
+ * loudness and the gain needed to hit a target, or isolate vocals. Maps to
813
+ * Montaj's four underlying audio-polish steps.
814
+ *
815
+ * Promise-based, not an async iterable, and deliberately so: these are
816
+ * polled jobs with no log stream, unlike `generateCaptions`'s streaming
817
+ * transcription — modeled instead on `getWaveformPeaks`/`getFilmstrip`.
818
+ *
819
+ * Optional so Hub keeps compiling against the released `@bycrux/editor`
820
+ * unchanged; the UI hides the audio-polish entry point on hosts that omit
821
+ * this, the same `generateCaptions` precedent. Hosts without an
822
+ * audio-polish pipeline simply don't implement it.
823
+ *
824
+ * `args.window` and every time in the result are **source** time — offsets
825
+ * into `args.src`, never timeline time (see `AudioPolishAnalysis`). This is
826
+ * the single easiest way to misuse this method; a caller must convert
827
+ * through the clip's own timeline↔source mapping before applying a
828
+ * removal/keep to the project.
829
+ */
830
+ analyzeAudioPolish?(args: AnalyzeAudioPolishArgs): Promise<AudioPolishAnalysis>
404
831
  }
405
832
 
406
833
  // ── Theme ────────────────────────────────────────────────────────────────────
@@ -462,6 +889,19 @@ export interface EditorSlots {
462
889
  exportActions?: ReactNode
463
890
  /** Rendered into the editor's assets/media panel area. */
464
891
  assetsPanel?: ReactNode
892
+ /**
893
+ * Rendered in the left media column of the CapCut layout. When present, the
894
+ * editor renders the three-column + full-width-timeline layout; otherwise
895
+ * the classic layout is unchanged.
896
+ */
897
+ mediaPanel?: ReactNode
898
+ /**
899
+ * Rendered in the CapCut layout's right properties panel when nothing is
900
+ * selected, in place of the editor's generic centered "Select an element"
901
+ * empty state. Hosts use it to brand the empty panel (Montaj shows its
902
+ * logo). Absent → the generic default shows. No effect in the classic layout.
903
+ */
904
+ propertiesEmptyState?: ReactNode
465
905
  /**
466
906
  * Rendered in the pending/empty view in place of the default
467
907
  * "Message your agent to start" copy. Hosts use this to surface live agent
@@ -573,23 +1013,38 @@ export interface VideoEditorProps<P extends Project = Project> {
573
1013
  */
574
1014
  onProvideRenderTrigger?: (openRender: () => void) => void
575
1015
 
1016
+ /**
1017
+ * Opt a host OUT of the package's built-in toolbar image-tone button so it
1018
+ * can surface the setting in its own chrome (e.g. the top-of-page header).
1019
+ * Mirrors `onProvideRenderTrigger`: when provided, the package (a) hides its
1020
+ * toolbar control and (b) calls this callback with the current state
1021
+ * whenever it changes. The host renders its own control (the package exports
1022
+ * `ImageToneMenu` with `variant="header"` for exactly this) and calls
1023
+ * `set(tone)` to persist a choice; the editor owns the save path.
1024
+ *
1025
+ * Called with `null` when the control should not be shown (SDR project: the
1026
+ * tone only affects HDR renders). Hosts must render nothing in that case.
1027
+ */
1028
+ onProvideImageTone?: (api: { value: ImageTone; set: (tone: ImageTone) => void } | null) => void
1029
+
576
1030
  // ── Host-supplied Montaj-specific UI (render-prop seams) ──────────────────
577
- // The clip/audio inspector and the subcut-regeneration tool read host-only
1031
+ // The generation panel and the subcut-regeneration tool read host-only
578
1032
  // fields (regenQueue, storyboard, the host's full Project) the package types
579
1033
  // don't know. The editor surfaces them as render-props it threads/renders so
580
1034
  // those components can stay host-side; the editor stays Montaj-agnostic.
1035
+ // Both take the clip id rather than a project entity — the editor owns the
1036
+ // selection, the host owns what to draw for it.
581
1037
 
582
1038
  /**
583
- * Render-prop seam for the host's clip/audio inspector (Montaj's
584
- * ClipInspectModal). The editor owns the "which item is being inspected"
585
- * state — it derives `ctx.item` from the timeline's `onInspectClip` /
586
- * `onInspectAudio` callbacks (a Montaj-agnostic `{ kind, id }` selector, not
587
- * a project entity) and passes a close callback. Absent → no inspector.
1039
+ * Render-prop seam for the host's per-clip generation panel (Montaj's AI
1040
+ * regenerate surface), rendered inside the right properties panel beneath
1041
+ * the clip properties whenever a VIDEO clip is selected. It reads and writes
1042
+ * `project.regenQueue` and `project.storyboard` — host-only fields this
1043
+ * package deliberately knows nothing about (see EditorProject's index-
1044
+ * signature comment) — so the content stays host-side and the editor only
1045
+ * says WHERE it goes and WHICH clip it is for. Absent → nothing rendered.
588
1046
  */
589
- renderClipInspector?: (ctx: {
590
- item: { kind: 'clip' | 'audio'; id: string }
591
- onClose: () => void
592
- }) => ReactNode
1047
+ renderGenerationPanel?: (ctx: { clipId: string }) => ReactNode
593
1048
 
594
1049
  /**
595
1050
  * Render-prop seam for the host's subcut-regeneration tool (Montaj's
@@ -612,4 +1067,128 @@ export interface VideoEditorProps<P extends Project = Project> {
612
1067
  * reads `regenQueue`.
613
1068
  */
614
1069
  isClipQueued?: (itemId: string) => boolean
1070
+
1071
+ /**
1072
+ * SP4 — opt into the WebCodecs playback engine for the video preview.
1073
+ * Follows the `assetsPlacement`/`regenEnabled` host-knob precedent: an
1074
+ * optional prop, absent by default, that a host passes to change editor
1075
+ * behavior. Threaded straight through to `PreviewPlayer`'s own `engine`
1076
+ * prop (see `video/preview/PreviewPlayer.tsx`).
1077
+ *
1078
+ * Default (prop omitted) or `{ enabled: false }`: the legacy `<video>`-slot
1079
+ * player, completely unchanged — this is the non-regression guarantee the
1080
+ * SP4 plan tests against (the entire editor suite stays green with this
1081
+ * prop untouched).
1082
+ *
1083
+ * `{ enabled: true }` does not itself force engine mode: the editor
1084
+ * evaluates per-project eligibility (`engine/eligibility.ts` — WebCodecs
1085
+ * avc1/opus decode support, plus every track-0 video item proxied and none
1086
+ * requiring the WebM `nobg_preview_src` alpha path) once per project load,
1087
+ * and falls back to the legacy player, reasoned via console, whenever a
1088
+ * project doesn't pass. `debugHud` additionally renders the
1089
+ * fps/drops/buffer/clock-kind readout; it has no effect while `enabled` is
1090
+ * false.
1091
+ *
1092
+ * No flag mechanism existed before this — hosts opt in explicitly, and this
1093
+ * prop stays absent-by-default for every consumer of the package. The montaj
1094
+ * ui app passes `enabled: true` unconditionally; other hosts are unaffected
1095
+ * and must still opt in.
1096
+ */
1097
+ engine?: { enabled: boolean; debugHud?: boolean }
1098
+
1099
+ /**
1100
+ * Opt-in seam letting a host's footage bin drive the MAIN preview on hover.
1101
+ * When the host hovers a bin clip card it sets `{ url, fraction }` on this
1102
+ * store; the editor mounts a paused `<video>` overlay above the preview and
1103
+ * seeks it to `fraction × duration`, so the operator source-scrubs an
1104
+ * OFF-TIMELINE clip without disturbing the playhead. Clearing it to `null`
1105
+ * unmounts the overlay and the normal timeline preview shows again.
1106
+ *
1107
+ * Default (prop omitted): totally inert — no overlay is ever mounted and the
1108
+ * main preview behaves exactly as before, so the classic layout, Hub and LP
1109
+ * are unaffected. A host opts in by creating the store
1110
+ * (`createSourcePreviewStore`) and passing it here AND to the card that writes
1111
+ * to it. Mirrors the `hover-scrub` store pattern; see
1112
+ * `video/source-preview.ts`.
1113
+ */
1114
+ sourcePreview?: SourcePreviewStore
1115
+
1116
+ /**
1117
+ * Opt-in seam for a non-blocking, host-driven caption job. When provided,
1118
+ * the editor delegates the caption generate/regenerate trigger to this
1119
+ * callback instead of opening its own blocking `CaptionRegenModal` — the
1120
+ * host is asserting it owns the job (e.g. running it as a background task
1121
+ * and reconciling `project.captions` itself via its own transport). Wins
1122
+ * over `adapter.generateCaptions` when both are present, since a host that
1123
+ * passes this prop typically still implements `generateCaptions` to power
1124
+ * the job it triggers.
1125
+ *
1126
+ * Absent (the default): the editor's existing built-in `CaptionRegenModal`
1127
+ * path runs completely unchanged — this is the Hub/Los Parceros backward-
1128
+ * compat guarantee. Neither host currently passes this prop.
1129
+ */
1130
+ onRegenerateCaptions?: () => void
1131
+ /**
1132
+ * Lets the host tell the caption panel that ITS background caption job is
1133
+ * in flight, so the generate/regenerate trigger button disables while it
1134
+ * runs. Meaningful only alongside `onRegenerateCaptions` — a host that owns
1135
+ * the trigger also owns knowing when the job is still running, since the
1136
+ * editor has no visibility into a job it didn't start.
1137
+ *
1138
+ * OR'd with the editor's own internal modal-open state at the call site —
1139
+ * it never replaces that state, only adds to it, so the built-in modal's
1140
+ * "disable the trigger while it's open" behavior keeps working even when a
1141
+ * host also sets this. Absent → treated as `false`, no effect.
1142
+ */
1143
+ captionsGenerating?: boolean
1144
+
1145
+ // ── Filesystem drop onto the timeline (opt-in) ────────────────────────────
1146
+ // Both halves of the same seam, and both FEATURE-DETECTED: a host that
1147
+ // passes neither is byte-unchanged, because the surface only claims an
1148
+ // OS-file drag when the hook below is actually present (see the note on it).
1149
+
1150
+ /**
1151
+ * A drop of real FILES from the OS onto the timeline. Absent → an OS-file
1152
+ * drag is not accepted at all and the browser keeps its default handling,
1153
+ * which is exactly what a host that predates this feature gets.
1154
+ *
1155
+ * The package hands over only what the browser told IT, plus the one piece
1156
+ * of its own state the host cannot see:
1157
+ * - `placement.atTime` — the timeline second under the pointer.
1158
+ * - `placement.preferredTrackIndex` — the video row released over, in
1159
+ * NORMALIZED track order, or `-1` when the pointer was over the ruler, a
1160
+ * caption band, an audio lane or the gap between rows.
1161
+ * - `placement.ripple` — the editor's ripple/magnet mode, CAPTURED AT DROP
1162
+ * TIME rather than read live. This is deliberate and is the whole reason
1163
+ * the field exists: `rippleMode` is internal editor state with no other
1164
+ * route to the host, and the host places the clip seconds later, when its
1165
+ * background import resolves — by which point the operator may well have
1166
+ * toggled the magnet again. The mode in force during the GESTURE is what
1167
+ * the user meant by that drop, so that is the one that has to survive the
1168
+ * wait. Feed it straight back into `placeDroppedClip`'s `ripple`.
1169
+ *
1170
+ * Fire-and-forget: the editor does not wait on this and does not mutate the
1171
+ * project for it. Importing a filesystem file means probing it, normalizing
1172
+ * it and building a proxy — all host-side work, of unbounded duration — so
1173
+ * the host owns the whole job and commits the resulting clip itself (route
1174
+ * it through the exported `placeDroppedClip` with this same `atTime` /
1175
+ * `preferredTrackIndex` and the placement will match what the drop
1176
+ * indicated). Feed `pendingDrops` below while that runs.
1177
+ */
1178
+ onImportFilesToTimeline?: (files: File[], placement: TimelineDropPlacement) => void
1179
+
1180
+ /**
1181
+ * Ghost bands for imports still in flight — one per file the host has
1182
+ * accepted from `onImportFilesToTimeline` and not yet landed as a real clip.
1183
+ * Drawn on the timeline's overlay layer as a dashed, translucent band at the
1184
+ * drop point, so a slow import is visibly "coming" at the place it was
1185
+ * dropped rather than nothing at all until it appears.
1186
+ *
1187
+ * The host owns the list entirely: it adds an entry when it starts an
1188
+ * import and drops that entry when the import lands (or fails). The package
1189
+ * never adds, mutates or expires one — a ghost that is never retracted stays
1190
+ * on screen forever, which is the host's bug to fix, not something the
1191
+ * editor guesses at. Absent or empty → nothing is drawn.
1192
+ */
1193
+ pendingDrops?: readonly PendingDrop[]
615
1194
  }