@pixerate/editor-react 0.55.0 → 0.57.0

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.
@@ -41,11 +41,11 @@ function useVideoEditor(controllerOrOptions = {}) {
41
41
  return { controller, state, ...actions };
42
42
  }
43
43
  function useVideoEditorTools(registry, controller, options = {}) {
44
- const { id, namespace, readOnly, rasterize } = options;
44
+ const { id, namespace, readOnly, rasterize, framePixels, framePixelsTimeout } = options;
45
45
  useEffect(() => {
46
46
  if (!registry) return;
47
- return registry.register(createVideoEditorToolProvider(controller, { id, namespace, readOnly, rasterize }));
48
- }, [registry, controller, id, namespace, readOnly, rasterize]);
47
+ return registry.register(createVideoEditorToolProvider(controller, { id, namespace, readOnly, rasterize, framePixels, framePixelsTimeout }));
48
+ }, [registry, controller, id, namespace, readOnly, rasterize, framePixels, framePixelsTimeout]);
49
49
  }
50
50
 
51
51
  // src/video-editor/VideoPreview.tsx
@@ -76,28 +76,96 @@ function VideoPreview({ controller, resolution, onFrame, className = "", style }
76
76
  );
77
77
  }
78
78
 
79
+ // src/video-editor/VideoScopes.tsx
80
+ import { useEffect as useEffect3, useRef as useRef3, useState } from "react";
81
+ import { readVideoScopesPanel, VIDEO_SCOPE_KINDS } from "@pixerate/editor/video-editor";
82
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
83
+ var LABELS = { histogram: "Histogram", waveform: "Luma waveform", vectorscope: "Vectorscope" };
84
+ var pct = (v) => `${Math.round(v * 1e3) / 10}%`;
85
+ function VideoScopes({ controller, scopes = VIDEO_SCOPE_KINDS, width = 256, height = 128, className = "" }) {
86
+ const canvases = useRef3({});
87
+ const [reading, setReading] = useState(() => ({ ok: false, message: "Show the preview to see its scopes." }));
88
+ const key = scopes.join(",");
89
+ useEffect3(() => {
90
+ const feed = controller.previewPixels;
91
+ let frame = 0;
92
+ const raf = typeof requestAnimationFrame === "function" ? requestAnimationFrame : (fn) => setTimeout(fn, 16);
93
+ const caf = typeof cancelAnimationFrame === "function" ? cancelAnimationFrame : (id) => clearTimeout(id);
94
+ const draw = () => {
95
+ frame = 0;
96
+ const next = readVideoScopesPanel(feed, scopes, { width, height });
97
+ setReading(next);
98
+ if (!next.ok) return;
99
+ for (const kind of scopes) {
100
+ const img = next.images[kind];
101
+ const ctx = canvases.current[kind]?.getContext("2d");
102
+ if (img && ctx && typeof ImageData !== "undefined") ctx.putImageData(new ImageData(img.data, img.width, img.height), 0, 0);
103
+ }
104
+ };
105
+ const schedule = () => {
106
+ if (!frame) frame = raf(draw);
107
+ };
108
+ schedule();
109
+ const off = feed.subscribe(schedule);
110
+ return () => {
111
+ off();
112
+ if (frame) caf(frame);
113
+ };
114
+ }, [controller, key, width, height]);
115
+ return /* @__PURE__ */ jsxs("div", { className: `flex flex-col gap-2 text-xs ${className}`, "data-video-scopes": "", children: [
116
+ /* @__PURE__ */ jsx2("div", { className: "flex flex-wrap gap-2", children: scopes.map((kind) => {
117
+ const w = kind === "vectorscope" ? height : width;
118
+ return /* @__PURE__ */ jsxs("figure", { className: "m-0 flex min-w-0 max-w-full flex-col gap-1", children: [
119
+ /* @__PURE__ */ jsx2("canvas", { ref: (el) => void (canvases.current[kind] = el), width: w, height, className: "rounded border border-border bg-black", style: { width: w, maxWidth: "100%", height: "auto", aspectRatio: `${w} / ${height}` }, "aria-label": LABELS[kind], "data-scope": kind }),
120
+ /* @__PURE__ */ jsx2("figcaption", { className: "text-[11px] text-muted-foreground", children: LABELS[kind] })
121
+ ] }, kind);
122
+ }) }),
123
+ reading.ok ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-0.5", "data-scope-stats": "", children: [
124
+ /* @__PURE__ */ jsxs("div", { className: "tabular-nums", children: [
125
+ "Luma ",
126
+ reading.stats.luma.mean.toFixed(2),
127
+ " mean \xB7 ",
128
+ reading.stats.luma.p1.toFixed(2),
129
+ "\u2013",
130
+ reading.stats.luma.p99.toFixed(2),
131
+ " \xB7 crushed ",
132
+ pct(reading.stats.clipped.shadows),
133
+ " \xB7 blown ",
134
+ pct(reading.stats.clipped.highlights),
135
+ " \xB7 average",
136
+ " ",
137
+ /* @__PURE__ */ jsx2("span", { className: "inline-block h-2.5 w-2.5 rounded-sm align-middle", style: { background: reading.stats.average } }),
138
+ " ",
139
+ reading.stats.average,
140
+ !reading.complete && /* @__PURE__ */ jsx2("span", { className: "text-muted-foreground", children: " \xB7 loading" })
141
+ ] }),
142
+ reading.notes.map((n) => /* @__PURE__ */ jsx2("div", { className: "text-amber-500", children: n }, n))
143
+ ] }) : /* @__PURE__ */ jsx2("div", { className: "text-muted-foreground", "data-scope-message": "", children: reading.message })
144
+ ] });
145
+ }
146
+
79
147
  // src/video-editor/VideoTransport.tsx
80
148
  import { formatVideoTimecode } from "@pixerate/editor/video-editor";
81
- import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
149
+ import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
82
150
  var btn = "rounded border border-border px-2 py-0.5 text-xs disabled:opacity-40";
83
151
  function VideoTransport({ controller, editable = true, className = "" }) {
84
152
  const state = useVideoEditorState(controller);
85
153
  const { time, duration, playing, project, selection, readOnly } = state;
86
154
  const canEdit = editable && !readOnly;
87
- return /* @__PURE__ */ jsxs("div", { className: `flex flex-wrap items-center gap-1.5 text-xs ${className}`, "data-video-transport": "", children: [
88
- /* @__PURE__ */ jsx2("button", { type: "button", className: btn, onClick: () => controller.step(-1), "aria-label": "Previous frame", title: "Previous frame (\u2190)", "data-step-back": "", children: "\u25C0|" }),
89
- /* @__PURE__ */ jsx2("button", { type: "button", className: btn, onClick: () => controller.toggle(), "aria-label": playing ? "Pause" : "Play", title: "Play / pause (Space)", disabled: duration <= 0, "data-play": "", children: playing ? "Pause" : "Play" }),
90
- /* @__PURE__ */ jsx2("button", { type: "button", className: btn, onClick: () => controller.step(1), "aria-label": "Next frame", title: "Next frame (\u2192)", "data-step-forward": "", children: "|\u25B6" }),
91
- /* @__PURE__ */ jsxs("span", { className: "px-1 tabular-nums text-muted-foreground", "data-timecode": "", children: [
155
+ return /* @__PURE__ */ jsxs2("div", { className: `flex flex-wrap items-center gap-1.5 text-xs ${className}`, "data-video-transport": "", children: [
156
+ /* @__PURE__ */ jsx3("button", { type: "button", className: btn, onClick: () => controller.step(-1), "aria-label": "Previous frame", title: "Previous frame (\u2190)", "data-step-back": "", children: "\u25C0|" }),
157
+ /* @__PURE__ */ jsx3("button", { type: "button", className: btn, onClick: () => controller.toggle(), "aria-label": playing ? "Pause" : "Play", title: "Play / pause (Space)", disabled: duration <= 0, "data-play": "", children: playing ? "Pause" : "Play" }),
158
+ /* @__PURE__ */ jsx3("button", { type: "button", className: btn, onClick: () => controller.step(1), "aria-label": "Next frame", title: "Next frame (\u2192)", "data-step-forward": "", children: "|\u25B6" }),
159
+ /* @__PURE__ */ jsxs2("span", { className: "px-1 tabular-nums text-muted-foreground", "data-timecode": "", children: [
92
160
  formatVideoTimecode(time, project.fps),
93
161
  " / ",
94
162
  formatVideoTimecode(duration, project.fps)
95
163
  ] }),
96
- editable && /* @__PURE__ */ jsxs(Fragment, { children: [
97
- /* @__PURE__ */ jsx2("span", { className: "mx-1 h-4 w-px bg-border" }),
98
- /* @__PURE__ */ jsx2("button", { type: "button", className: btn, onClick: () => controller.splitAtPlayhead(), disabled: !canEdit, title: "Split at the playhead (S)", "data-split": "", children: "Split" }),
99
- /* @__PURE__ */ jsx2("button", { type: "button", className: btn, onClick: () => controller.deleteSelection(), disabled: !canEdit || !selection.length, title: "Delete the selected clips (Delete)", "data-delete": "", children: "Delete" }),
100
- /* @__PURE__ */ jsx2(
164
+ editable && /* @__PURE__ */ jsxs2(Fragment, { children: [
165
+ /* @__PURE__ */ jsx3("span", { className: "mx-1 h-4 w-px bg-border" }),
166
+ /* @__PURE__ */ jsx3("button", { type: "button", className: btn, onClick: () => controller.splitAtPlayhead(), disabled: !canEdit, title: "Split at the playhead (S)", "data-split": "", children: "Split" }),
167
+ /* @__PURE__ */ jsx3("button", { type: "button", className: btn, onClick: () => controller.deleteSelection(), disabled: !canEdit || !selection.length, title: "Delete the selected clips (Delete)", "data-delete": "", children: "Delete" }),
168
+ /* @__PURE__ */ jsx3(
101
169
  "button",
102
170
  {
103
171
  type: "button",
@@ -109,14 +177,14 @@ function VideoTransport({ controller, editable = true, className = "" }) {
109
177
  children: "Ripple delete"
110
178
  }
111
179
  ),
112
- /* @__PURE__ */ jsx2("button", { type: "button", className: btn, onClick: () => controller.undo(), disabled: !canEdit || !state.canUndo, title: state.canUndo ? `Undo ${state.undoLabel}` : "Nothing to undo", "data-undo": "", children: "Undo" }),
113
- /* @__PURE__ */ jsx2("button", { type: "button", className: btn, onClick: () => controller.redo(), disabled: !canEdit || !state.canRedo, title: state.canRedo ? `Redo ${state.redoLabel}` : "Nothing to redo", "data-redo": "", children: "Redo" })
180
+ /* @__PURE__ */ jsx3("button", { type: "button", className: btn, onClick: () => controller.undo(), disabled: !canEdit || !state.canUndo, title: state.canUndo ? `Undo ${state.undoLabel}` : "Nothing to undo", "data-undo": "", children: "Undo" }),
181
+ /* @__PURE__ */ jsx3("button", { type: "button", className: btn, onClick: () => controller.redo(), disabled: !canEdit || !state.canRedo, title: state.canRedo ? `Redo ${state.redoLabel}` : "Nothing to redo", "data-redo": "", children: "Redo" })
114
182
  ] })
115
183
  ] });
116
184
  }
117
185
 
118
186
  // src/video-editor/VideoTimeline.tsx
119
- import { useEffect as useEffect3, useMemo as useMemo2, useRef as useRef3, useState, useSyncExternalStore as useSyncExternalStore2 } from "react";
187
+ import { useEffect as useEffect4, useMemo as useMemo2, useRef as useRef4, useState as useState2, useSyncExternalStore as useSyncExternalStore2 } from "react";
120
188
  import {
121
189
  planVideoClipDrag,
122
190
  videoDragLabel,
@@ -129,7 +197,7 @@ import {
129
197
  videoRulerTicks,
130
198
  formatVideoSeconds
131
199
  } from "@pixerate/editor/video-editor";
132
- import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
200
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
133
201
  var HEADER = 150;
134
202
  var RULER = 24;
135
203
  var ROW = 44;
@@ -143,25 +211,25 @@ function ClipWaveform({ controller, clip, asset, width, height }) {
143
211
  () => controller.audioPeaks.peek(asset),
144
212
  () => void 0
145
213
  );
146
- useEffect3(() => {
214
+ useEffect4(() => {
147
215
  controller.audioPeaks.load(asset).catch(() => {
148
216
  });
149
217
  }, [controller, asset]);
150
218
  const d = useMemo2(() => peaks ? videoClipWaveformPath(clip, peaks, width, height) : "", [peaks, clip, width, height]);
151
219
  if (!d) return null;
152
- return /* @__PURE__ */ jsx3("svg", { className: "pointer-events-none absolute inset-0", width, height, "aria-hidden": "true", "data-waveform": clip.id, children: /* @__PURE__ */ jsx3("path", { d, fill: "currentColor", fillOpacity: 0.45 }) });
220
+ return /* @__PURE__ */ jsx4("svg", { className: "pointer-events-none absolute inset-0", width, height, "aria-hidden": "true", "data-waveform": clip.id, children: /* @__PURE__ */ jsx4("path", { d, fill: "currentColor", fillOpacity: 0.45 }) });
153
221
  }
154
222
  var isField = (el) => el instanceof HTMLElement && /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName);
155
223
  function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable = true, waveforms = "audio", className = "" }) {
156
224
  const state = useVideoEditorState(controller);
157
225
  const { project, duration, time, selection, readOnly, error } = state;
158
- const [ownZoom, setOwnZoom] = useState(pxPerSecond ?? 100);
226
+ const [ownZoom, setOwnZoom] = useState2(pxPerSecond ?? 100);
159
227
  const px = onPxPerSecondChange ? pxPerSecond ?? 100 : ownZoom;
160
228
  const canEdit = editable && !readOnly;
161
- const [drag, setDrag] = useState(null);
162
- const dragRef = useRef3(null);
163
- const rows = useRef3(/* @__PURE__ */ new Map());
164
- const ruler = useRef3(null);
229
+ const [drag, setDrag] = useState2(null);
230
+ const dragRef = useRef4(null);
231
+ const rows = useRef4(/* @__PURE__ */ new Map());
232
+ const ruler = useRef4(null);
165
233
  const assets = videoAssetMap(project);
166
234
  const msToPx = (ms) => ms * px / 1e3;
167
235
  const span = Math.max(duration + 5e3, 1e4);
@@ -192,10 +260,10 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
192
260
  dragRef.current = next;
193
261
  setDrag(next);
194
262
  };
195
- const live = useRef3({ planFor, selection });
263
+ const live = useRef4({ planFor, selection });
196
264
  live.current = { planFor, selection };
197
- const stopDrag = useRef3(null);
198
- useEffect3(() => () => stopDrag.current?.(), []);
265
+ const stopDrag = useRef4(null);
266
+ useEffect4(() => () => stopDrag.current?.(), []);
199
267
  const onClipPointerDown = (e, clip, track) => {
200
268
  if (e.button !== void 0 && e.button !== 0) return;
201
269
  e.stopPropagation();
@@ -273,7 +341,7 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
273
341
  }).filter((x) => x !== null)
274
342
  )
275
343
  }));
276
- const toggle = (track, field2, label) => /* @__PURE__ */ jsx3(
344
+ const toggle = (track, field2, label) => /* @__PURE__ */ jsx4(
277
345
  "button",
278
346
  {
279
347
  type: "button",
@@ -287,12 +355,12 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
287
355
  children: field2 === "muted" ? "M" : field2 === "hidden" ? "H" : "L"
288
356
  }
289
357
  );
290
- return /* @__PURE__ */ jsxs2("div", { className: `flex flex-col gap-1 ${className}`, "data-video-timeline-wrapper": "", children: [
291
- !onPxPerSecondChange && /* @__PURE__ */ jsxs2("label", { className: "flex items-center gap-2 self-end text-xs text-muted-foreground", children: [
358
+ return /* @__PURE__ */ jsxs3("div", { className: `flex flex-col gap-1 ${className}`, "data-video-timeline-wrapper": "", children: [
359
+ !onPxPerSecondChange && /* @__PURE__ */ jsxs3("label", { className: "flex items-center gap-2 self-end text-xs text-muted-foreground", children: [
292
360
  "Zoom",
293
- /* @__PURE__ */ jsx3("input", { type: "range", min: MIN_ZOOM, max: MAX_ZOOM, step: 5, value: ownZoom, "aria-label": "Timeline zoom", onChange: (e) => setOwnZoom(Number(e.target.value)), "data-zoom": "" })
361
+ /* @__PURE__ */ jsx4("input", { type: "range", min: MIN_ZOOM, max: MAX_ZOOM, step: 5, value: ownZoom, "aria-label": "Timeline zoom", onChange: (e) => setOwnZoom(Number(e.target.value)), "data-zoom": "" })
294
362
  ] }),
295
- /* @__PURE__ */ jsx3(
363
+ /* @__PURE__ */ jsx4(
296
364
  "div",
297
365
  {
298
366
  className: "relative overflow-x-auto rounded border border-border outline-none focus-visible:ring-2 focus-visible:ring-rose-500/50",
@@ -301,10 +369,10 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
301
369
  "aria-label": "Video timeline",
302
370
  onKeyDown,
303
371
  "data-video-timeline": "",
304
- children: /* @__PURE__ */ jsxs2("div", { className: "relative", style: { width: HEADER + width }, children: [
305
- /* @__PURE__ */ jsxs2("div", { className: "flex", style: { height: RULER }, children: [
306
- /* @__PURE__ */ jsx3("div", { className: "sticky left-0 z-20 shrink-0 border-b border-r border-border bg-background", style: { width: HEADER } }),
307
- /* @__PURE__ */ jsxs2(
372
+ children: /* @__PURE__ */ jsxs3("div", { className: "relative", style: { width: HEADER + width }, children: [
373
+ /* @__PURE__ */ jsxs3("div", { className: "flex", style: { height: RULER }, children: [
374
+ /* @__PURE__ */ jsx4("div", { className: "sticky left-0 z-20 shrink-0 border-b border-r border-border bg-background", style: { width: HEADER } }),
375
+ /* @__PURE__ */ jsxs3(
308
376
  "div",
309
377
  {
310
378
  ref: ruler,
@@ -327,8 +395,8 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
327
395
  if (e.buttons & 1) seekAt(e.clientX);
328
396
  },
329
397
  children: [
330
- ticks.map((t) => /* @__PURE__ */ jsx3("div", { className: `absolute bottom-0 w-px ${t.major ? "h-2.5 bg-muted-foreground/70" : "h-1 bg-muted-foreground/40"}`, style: { left: t.x }, "data-tick": t.time, children: t.label && /* @__PURE__ */ jsx3("span", { className: "absolute -top-3.5 left-1 whitespace-nowrap tabular-nums", children: t.label }) }, t.time)),
331
- (project.markers ?? []).map((m) => /* @__PURE__ */ jsx3(
398
+ ticks.map((t) => /* @__PURE__ */ jsx4("div", { className: `absolute bottom-0 w-px ${t.major ? "h-2.5 bg-muted-foreground/70" : "h-1 bg-muted-foreground/40"}`, style: { left: t.x }, "data-tick": t.time, children: t.label && /* @__PURE__ */ jsx4("span", { className: "absolute -top-3.5 left-1 whitespace-nowrap tabular-nums", children: t.label }) }, t.time)),
399
+ (project.markers ?? []).map((m) => /* @__PURE__ */ jsx4(
332
400
  "div",
333
401
  {
334
402
  className: "absolute top-0 h-2.5 w-2.5 -translate-x-1/2 rotate-45",
@@ -342,19 +410,19 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
342
410
  }
343
411
  )
344
412
  ] }),
345
- placed.map(({ track, clips }) => /* @__PURE__ */ jsxs2("div", { className: "flex border-b border-border last:border-b-0", style: { height: ROW }, "data-track": track.id, children: [
346
- /* @__PURE__ */ jsxs2("div", { className: "sticky left-0 z-20 flex shrink-0 flex-col justify-center gap-0.5 border-r border-border bg-background px-2 text-xs", style: { width: HEADER }, "data-track-header": "", children: [
347
- /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-1", children: [
348
- /* @__PURE__ */ jsx3("span", { className: "truncate font-medium", children: track.name ?? track.id }),
349
- /* @__PURE__ */ jsx3("span", { className: "rounded bg-muted px-1 text-[10px] uppercase text-muted-foreground", "data-track-kind": "", children: track.kind })
413
+ placed.map(({ track, clips }) => /* @__PURE__ */ jsxs3("div", { className: "flex border-b border-border last:border-b-0", style: { height: ROW }, "data-track": track.id, children: [
414
+ /* @__PURE__ */ jsxs3("div", { className: "sticky left-0 z-20 flex shrink-0 flex-col justify-center gap-0.5 border-r border-border bg-background px-2 text-xs", style: { width: HEADER }, "data-track-header": "", children: [
415
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-1", children: [
416
+ /* @__PURE__ */ jsx4("span", { className: "truncate font-medium", children: track.name ?? track.id }),
417
+ /* @__PURE__ */ jsx4("span", { className: "rounded bg-muted px-1 text-[10px] uppercase text-muted-foreground", "data-track-kind": "", children: track.kind })
350
418
  ] }),
351
- editable && /* @__PURE__ */ jsxs2("div", { className: "flex gap-1", children: [
419
+ editable && /* @__PURE__ */ jsxs3("div", { className: "flex gap-1", children: [
352
420
  toggle(track, "muted", "Mute"),
353
421
  track.kind === "video" && toggle(track, "hidden", "Hide"),
354
422
  toggle(track, "locked", "Lock")
355
423
  ] })
356
424
  ] }),
357
- /* @__PURE__ */ jsx3(
425
+ /* @__PURE__ */ jsx4(
358
426
  "div",
359
427
  {
360
428
  ref: (el) => {
@@ -371,7 +439,7 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
371
439
  const asset = clip.type === "media" ? assets.get(clip.asset) : void 0;
372
440
  const selected = selection.includes(clip.id);
373
441
  const label = videoClipLabel(clip, asset);
374
- return /* @__PURE__ */ jsxs2(
442
+ return /* @__PURE__ */ jsxs3(
375
443
  "div",
376
444
  {
377
445
  className: `absolute top-1 bottom-1 select-none touch-none overflow-hidden rounded text-[11px] text-white ${canEdit && !home.locked ? "cursor-grab" : "cursor-default"} ${dragging ? "opacity-80 shadow-lg" : ""}`,
@@ -390,15 +458,15 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
390
458
  "aria-selected": selected,
391
459
  onPointerDown: (e) => onClipPointerDown(e, clip, home),
392
460
  children: [
393
- videoClipShowsWaveform(clip, asset, waveforms) && /* @__PURE__ */ jsx3(ClipWaveform, { controller, clip, asset, width: Math.max(2, msToPx(clip.duration)), height: ROW - 8 }),
394
- /* @__PURE__ */ jsx3("span", { className: "pointer-events-none relative block truncate px-1.5 py-0.5 leading-4", children: label }),
395
- clip.transition && /* @__PURE__ */ jsxs2("span", { className: "pointer-events-none absolute left-1 bottom-0.5 rounded bg-black/40 px-1 text-[9px] leading-3", title: `${clip.transition.type} ${clip.transition.duration} ms`, "data-transition": clip.transition.type, children: [
461
+ videoClipShowsWaveform(clip, asset, waveforms) && /* @__PURE__ */ jsx4(ClipWaveform, { controller, clip, asset, width: Math.max(2, msToPx(clip.duration)), height: ROW - 8 }),
462
+ /* @__PURE__ */ jsx4("span", { className: "pointer-events-none relative block truncate px-1.5 py-0.5 leading-4", children: label }),
463
+ clip.transition && /* @__PURE__ */ jsxs3("span", { className: "pointer-events-none absolute left-1 bottom-0.5 rounded bg-black/40 px-1 text-[9px] leading-3", title: `${clip.transition.type} ${clip.transition.duration} ms`, "data-transition": clip.transition.type, children: [
396
464
  "\u29D3 ",
397
465
  clip.transition.type
398
466
  ] }),
399
- canEdit && !home.locked && /* @__PURE__ */ jsxs2(Fragment2, { children: [
400
- /* @__PURE__ */ jsx3("div", { className: "absolute inset-y-0 left-0 cursor-ew-resize bg-white/0 hover:bg-white/40", style: { width: HANDLE }, "data-handle": "start", "aria-hidden": "true" }),
401
- /* @__PURE__ */ jsx3("div", { className: "absolute inset-y-0 right-0 cursor-ew-resize bg-white/0 hover:bg-white/40", style: { width: HANDLE }, "data-handle": "end", "aria-hidden": "true" })
467
+ canEdit && !home.locked && /* @__PURE__ */ jsxs3(Fragment2, { children: [
468
+ /* @__PURE__ */ jsx4("div", { className: "absolute inset-y-0 left-0 cursor-ew-resize bg-white/0 hover:bg-white/40", style: { width: HANDLE }, "data-handle": "start", "aria-hidden": "true" }),
469
+ /* @__PURE__ */ jsx4("div", { className: "absolute inset-y-0 right-0 cursor-ew-resize bg-white/0 hover:bg-white/40", style: { width: HANDLE }, "data-handle": "end", "aria-hidden": "true" })
402
470
  ] })
403
471
  ]
404
472
  },
@@ -408,11 +476,11 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
408
476
  }
409
477
  )
410
478
  ] }, track.id)),
411
- /* @__PURE__ */ jsx3("div", { className: "pointer-events-none absolute top-0 bottom-0 w-0.5 -translate-x-1/2 bg-rose-500", style: { left: HEADER + msToPx(time), zIndex: 15 }, "data-playhead": "" })
479
+ /* @__PURE__ */ jsx4("div", { className: "pointer-events-none absolute top-0 bottom-0 w-0.5 -translate-x-1/2 bg-rose-500", style: { left: HEADER + msToPx(time), zIndex: 15 }, "data-playhead": "" })
412
480
  ] })
413
481
  }
414
482
  ),
415
- error && /* @__PURE__ */ jsx3("p", { className: "text-xs text-red-500", role: "alert", "data-edit-error": "", children: error })
483
+ error && /* @__PURE__ */ jsx4("p", { className: "text-xs text-red-500", role: "alert", "data-edit-error": "", children: error })
416
484
  ] });
417
485
  }
418
486
 
@@ -434,12 +502,16 @@ import {
434
502
  VIDEO_TRANSITION_TYPES,
435
503
  VIDEO_WIPE_DIRECTIONS,
436
504
  videoTransitionHasDirection,
505
+ videoFreezeFrameEdit,
437
506
  videoKeyframeRows,
507
+ videoSlowMotionRamp,
508
+ videoSpeedRampEdit,
438
509
  videoTransitionAudioCurve,
439
510
  videoKeyframeToggleEdit,
440
511
  videoKeyframeValueEdit
441
512
  } from "@pixerate/editor/video-editor";
442
- import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
513
+ import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
514
+ var btn2 = "rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40";
443
515
  var field = "w-full rounded border border-border bg-transparent px-1 py-0.5 text-xs disabled:opacity-50";
444
516
  var DEFAULT_TRANSITION = 500;
445
517
  function Commit({
@@ -452,7 +524,7 @@ function Commit({
452
524
  if (raw !== value) onCommit(raw);
453
525
  };
454
526
  if (multiline)
455
- return /* @__PURE__ */ jsx4(
527
+ return /* @__PURE__ */ jsx5(
456
528
  "textarea",
457
529
  {
458
530
  className: field,
@@ -464,11 +536,11 @@ function Commit({
464
536
  },
465
537
  value
466
538
  );
467
- return /* @__PURE__ */ jsx4("input", { className: field, ...rest, defaultValue: value, onKeyDown: (e) => e.key === "Enter" && e.currentTarget.blur(), onBlur: (e) => commit(e.currentTarget.value) }, value);
539
+ return /* @__PURE__ */ jsx5("input", { className: field, ...rest, defaultValue: value, onKeyDown: (e) => e.key === "Enter" && e.currentTarget.blur(), onBlur: (e) => commit(e.currentTarget.value) }, value);
468
540
  }
469
541
  function Row({ label, name, children }) {
470
- return /* @__PURE__ */ jsxs3("label", { className: "grid grid-cols-[5.5rem_minmax(0,1fr)] items-center gap-2 text-xs", "data-field": name, children: [
471
- /* @__PURE__ */ jsx4("span", { className: "text-muted-foreground", children: label }),
542
+ return /* @__PURE__ */ jsxs4("label", { className: "grid grid-cols-[5.5rem_minmax(0,1fr)] items-center gap-2 text-xs", "data-field": name, children: [
543
+ /* @__PURE__ */ jsx5("span", { className: "text-muted-foreground", children: label }),
472
544
  children
473
545
  ] });
474
546
  }
@@ -476,7 +548,7 @@ function ClipInspector({ controller, className = "" }) {
476
548
  const state = useVideoEditorState(controller);
477
549
  const { project, selection, readOnly, time } = state;
478
550
  if (selection.length !== 1)
479
- return /* @__PURE__ */ jsx4("p", { className: `text-xs text-muted-foreground ${className}`, "data-clip-inspector": "", children: selection.length ? `${selection.length} clips selected. Select one clip to edit it.` : "Select a clip on the timeline to edit it." });
551
+ return /* @__PURE__ */ jsx5("p", { className: `text-xs text-muted-foreground ${className}`, "data-clip-inspector": "", children: selection.length ? `${selection.length} clips selected. Select one clip to edit it.` : "Select a clip on the timeline to edit it." });
480
552
  const at = findVideoClip(project, selection[0]);
481
553
  if (!at) return null;
482
554
  const clip = at.clip;
@@ -493,24 +565,24 @@ function ClipInspector({ controller, className = "" }) {
493
565
  };
494
566
  const text = (key, label) => {
495
567
  const value = clip[key];
496
- return /* @__PURE__ */ jsx4(Commit, { value: value === void 0 ? "" : String(value), "aria-label": label ?? key, disabled, onCommit: (raw) => set(key, raw.trim() || void 0, `Change ${label ?? key}`) });
568
+ return /* @__PURE__ */ jsx5(Commit, { value: value === void 0 ? "" : String(value), "aria-label": label ?? key, disabled, onCommit: (raw) => set(key, raw.trim() || void 0, `Change ${label ?? key}`) });
497
569
  };
498
570
  const number = (key, label, attrs = {}) => {
499
571
  const value = clip[key];
500
- return /* @__PURE__ */ jsx4(Commit, { type: "number", value: value === void 0 ? "" : String(value), "aria-label": label, disabled, onCommit: (raw) => num(key, raw, `Change ${label.toLowerCase()}`), ...attrs });
572
+ return /* @__PURE__ */ jsx5(Commit, { type: "number", value: value === void 0 ? "" : String(value), "aria-label": label, disabled, onCommit: (raw) => num(key, raw, `Change ${label.toLowerCase()}`), ...attrs });
501
573
  };
502
574
  const transition = clip.transition;
503
- return /* @__PURE__ */ jsxs3("div", { className: `flex flex-col gap-1.5 ${className}`, "data-clip-inspector": "", "data-clip-id": clip.id, children: [
504
- /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2 text-xs", children: [
505
- /* @__PURE__ */ jsx4("span", { className: "font-medium", children: clip.type === "media" ? "Media clip" : clip.type === "text" ? "Title" : "Colour clip" }),
506
- /* @__PURE__ */ jsx4("code", { className: "text-muted-foreground", children: clip.id }),
507
- at.track.locked && /* @__PURE__ */ jsx4("span", { className: "text-amber-500", children: "locked track" })
575
+ return /* @__PURE__ */ jsxs4("div", { className: `flex flex-col gap-1.5 ${className}`, "data-clip-inspector": "", "data-clip-id": clip.id, children: [
576
+ /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2 text-xs", children: [
577
+ /* @__PURE__ */ jsx5("span", { className: "font-medium", children: clip.type === "media" ? "Media clip" : clip.type === "text" ? "Title" : "Colour clip" }),
578
+ /* @__PURE__ */ jsx5("code", { className: "text-muted-foreground", children: clip.id }),
579
+ at.track.locked && /* @__PURE__ */ jsx5("span", { className: "text-amber-500", children: "locked track" })
508
580
  ] }),
509
- /* @__PURE__ */ jsx4(Row, { label: "Name", name: "name", children: text("name", "Name") }),
510
- /* @__PURE__ */ jsx4(Row, { label: "Start", name: "start", children: /* @__PURE__ */ jsx4("span", { className: "tabular-nums text-xs", children: formatVideoSeconds2(clip.start) }) }),
511
- /* @__PURE__ */ jsx4(Row, { label: "Duration", name: "duration", children: /* @__PURE__ */ jsx4("span", { className: "tabular-nums text-xs", children: formatVideoSeconds2(clip.duration) }) }),
512
- videoKeyframeRows(project, clip.id, time).map((row) => /* @__PURE__ */ jsx4(Row, { label: row.label, name: row.property, children: /* @__PURE__ */ jsxs3("span", { className: "flex min-w-0 items-center gap-1", "data-keyframe-row": row.property, children: [
513
- /* @__PURE__ */ jsx4(
581
+ /* @__PURE__ */ jsx5(Row, { label: "Name", name: "name", children: text("name", "Name") }),
582
+ /* @__PURE__ */ jsx5(Row, { label: "Start", name: "start", children: /* @__PURE__ */ jsx5("span", { className: "tabular-nums text-xs", children: formatVideoSeconds2(clip.start) }) }),
583
+ /* @__PURE__ */ jsx5(Row, { label: "Duration", name: "duration", children: /* @__PURE__ */ jsx5("span", { className: "tabular-nums text-xs", children: formatVideoSeconds2(clip.duration) }) }),
584
+ videoKeyframeRows(project, clip.id, time).map((row) => /* @__PURE__ */ jsx5(Row, { label: row.label, name: row.property, children: /* @__PURE__ */ jsxs4("span", { className: "flex min-w-0 items-center gap-1", "data-keyframe-row": row.property, children: [
585
+ /* @__PURE__ */ jsx5(
514
586
  Commit,
515
587
  {
516
588
  type: "number",
@@ -529,7 +601,7 @@ function ClipInspector({ controller, className = "" }) {
529
601
  }
530
602
  }
531
603
  ),
532
- /* @__PURE__ */ jsx4(
604
+ /* @__PURE__ */ jsx5(
533
605
  "button",
534
606
  {
535
607
  type: "button",
@@ -546,11 +618,11 @@ function ClipInspector({ controller, className = "" }) {
546
618
  children: row.onKey ? "\u25C6" : "\u25C7"
547
619
  }
548
620
  ),
549
- row.keyed && /* @__PURE__ */ jsx4("span", { className: "shrink-0 text-[10px] tabular-nums text-muted-foreground", "data-keyframe-count": row.property, children: row.keys })
621
+ row.keyed && /* @__PURE__ */ jsx5("span", { className: "shrink-0 text-[10px] tabular-nums text-muted-foreground", "data-keyframe-count": row.property, children: row.keys })
550
622
  ] }) }, row.property)),
551
- at.track.kind === "video" && /* @__PURE__ */ jsx4(Row, { label: "Blend", name: "blend", children: /* @__PURE__ */ jsx4("select", { className: field, value: clip.blend ?? "normal", disabled, "aria-label": "Blend mode", onChange: (e) => set("blend", e.target.value === "normal" ? void 0 : e.target.value, "Change blend mode"), children: VIDEO_BLEND_MODES.map((m) => /* @__PURE__ */ jsx4("option", { value: m, children: VIDEO_BLEND_MODE_LABELS[m] }, m)) }) }),
552
- at.track.kind === "video" && /* @__PURE__ */ jsxs3(Fragment3, { children: [
553
- /* @__PURE__ */ jsx4(Row, { label: "Look", name: "gradePreset", children: /* @__PURE__ */ jsxs3(
623
+ at.track.kind === "video" && /* @__PURE__ */ jsx5(Row, { label: "Blend", name: "blend", children: /* @__PURE__ */ jsx5("select", { className: field, value: clip.blend ?? "normal", disabled, "aria-label": "Blend mode", onChange: (e) => set("blend", e.target.value === "normal" ? void 0 : e.target.value, "Change blend mode"), children: VIDEO_BLEND_MODES.map((m) => /* @__PURE__ */ jsx5("option", { value: m, children: VIDEO_BLEND_MODE_LABELS[m] }, m)) }) }),
624
+ at.track.kind === "video" && /* @__PURE__ */ jsxs4(Fragment3, { children: [
625
+ /* @__PURE__ */ jsx5(Row, { label: "Look", name: "gradePreset", children: /* @__PURE__ */ jsxs4(
554
626
  "select",
555
627
  {
556
628
  className: field,
@@ -562,12 +634,12 @@ function ClipInspector({ controller, className = "" }) {
562
634
  if (grade) controller.apply({ op: "update_clip", clip: clip.id, set: { grade } }, `Apply ${e.target.value} look`);
563
635
  },
564
636
  children: [
565
- /* @__PURE__ */ jsx4("option", { value: "", children: clip.grade ? "Graded" : "None" }),
566
- VIDEO_GRADE_PRESET_NAMES.map((name) => /* @__PURE__ */ jsx4("option", { value: name, children: name.replace(/-/g, " ") }, name))
637
+ /* @__PURE__ */ jsx5("option", { value: "", children: clip.grade ? "Graded" : "None" }),
638
+ VIDEO_GRADE_PRESET_NAMES.map((name) => /* @__PURE__ */ jsx5("option", { value: name, children: name.replace(/-/g, " ") }, name))
567
639
  ]
568
640
  }
569
641
  ) }),
570
- VIDEO_GRADE_CONTROLS.map((c) => /* @__PURE__ */ jsx4(Row, { label: c.label, name: `grade.${c.key}`, children: /* @__PURE__ */ jsx4(
642
+ VIDEO_GRADE_CONTROLS.map((c) => /* @__PURE__ */ jsx5(Row, { label: c.label, name: `grade.${c.key}`, children: /* @__PURE__ */ jsx5(
571
643
  Commit,
572
644
  {
573
645
  type: "number",
@@ -587,7 +659,7 @@ function ClipInspector({ controller, className = "" }) {
587
659
  }
588
660
  ) }, c.key))
589
661
  ] }),
590
- at.track.kind === "video" && ["animateIn", "animateOut"].map((key) => /* @__PURE__ */ jsx4(Row, { label: key === "animateIn" ? "Animate in" : "Animate out", name: key, children: /* @__PURE__ */ jsxs3(
662
+ at.track.kind === "video" && ["animateIn", "animateOut"].map((key) => /* @__PURE__ */ jsx5(Row, { label: key === "animateIn" ? "Animate in" : "Animate out", name: key, children: /* @__PURE__ */ jsxs4(
591
663
  "select",
592
664
  {
593
665
  className: field,
@@ -599,18 +671,35 @@ function ClipInspector({ controller, className = "" }) {
599
671
  set(key, effect ? { ...clip[key], effect } : void 0, key === "animateIn" ? "Change entrance" : "Change exit");
600
672
  },
601
673
  children: [
602
- /* @__PURE__ */ jsx4("option", { value: "", children: "None" }),
603
- VIDEO_CLIP_ANIMATION_EFFECTS.map((effect) => /* @__PURE__ */ jsx4("option", { value: effect, children: VIDEO_CLIP_ANIMATION_LABELS[effect] }, effect))
674
+ /* @__PURE__ */ jsx5("option", { value: "", children: "None" }),
675
+ VIDEO_CLIP_ANIMATION_EFFECTS.map((effect) => /* @__PURE__ */ jsx5("option", { value: effect, children: VIDEO_CLIP_ANIMATION_LABELS[effect] }, effect))
604
676
  ]
605
677
  }
606
678
  ) }, key)),
607
- /* @__PURE__ */ jsx4(Row, { label: "Fade in (ms)", name: "fadeIn", children: number("fadeIn", "Fade in", { min: 0, step: 100 }) }),
608
- /* @__PURE__ */ jsx4(Row, { label: "Fade out (ms)", name: "fadeOut", children: number("fadeOut", "Fade out", { min: 0, step: 100 }) }),
609
- clip.type === "media" && /* @__PURE__ */ jsxs3(Fragment3, { children: [
610
- /* @__PURE__ */ jsx4(Row, { label: "Speed", name: "speed", children: number("speed", "Speed", { min: 0.1, step: 0.25 }) }),
611
- /* @__PURE__ */ jsx4(Row, { label: "Muted", name: "muted", children: /* @__PURE__ */ jsx4("input", { type: "checkbox", className: "justify-self-start", checked: !!clip.muted, disabled, "aria-label": "Muted", onChange: (e) => set("muted", e.target.checked || void 0, e.target.checked ? "Mute clip" : "Unmute clip") }) }),
679
+ /* @__PURE__ */ jsx5(Row, { label: "Fade in (ms)", name: "fadeIn", children: number("fadeIn", "Fade in", { min: 0, step: 100 }) }),
680
+ /* @__PURE__ */ jsx5(Row, { label: "Fade out (ms)", name: "fadeOut", children: number("fadeOut", "Fade out", { min: 0, step: 100 }) }),
681
+ clip.type === "media" && /* @__PURE__ */ jsxs4(Fragment3, { children: [
682
+ /* @__PURE__ */ jsx5(Row, { label: "Speed", name: "speed", children: clip.timeRemap ? /* @__PURE__ */ jsxs4("span", { className: "flex items-center gap-1", "data-ramped": "", children: [
683
+ /* @__PURE__ */ jsx5("span", { className: "text-muted-foreground", children: "ramped" }),
684
+ /* @__PURE__ */ jsx5("button", { type: "button", className: btn2, disabled, onClick: () => controller.apply({ op: "set_time_remap", clip: clip.id, keys: null, ripple: true }, "Remove speed ramp"), children: "Remove ramp" })
685
+ ] }) : number("speed", "Speed", { min: 0.1, step: 0.25 }) }),
686
+ project.assets.find((a) => a.id === clip.asset)?.kind !== "image" && /* @__PURE__ */ jsx5(Row, { label: "Retime", name: "retime", children: /* @__PURE__ */ jsxs4("span", { className: "flex flex-wrap gap-1", children: [
687
+ /* @__PURE__ */ jsx5(
688
+ "button",
689
+ {
690
+ type: "button",
691
+ className: btn2,
692
+ disabled: disabled || time < clip.start || time > clip.start + clip.duration,
693
+ title: "Hold the frame at the playhead for a second, pushing later clips along",
694
+ onClick: () => controller.apply(videoFreezeFrameEdit(clip, time - clip.start, 1e3), "Freeze frame"),
695
+ children: "Freeze 1 s here"
696
+ }
697
+ ),
698
+ /* @__PURE__ */ jsx5("button", { type: "button", className: btn2, disabled, title: "Normal speed, then a glide into 0.3\xD7 slow motion", onClick: () => controller.apply(videoSpeedRampEdit(clip, videoSlowMotionRamp(clip.duration)), "Speed ramp"), children: "Slow-mo ramp" })
699
+ ] }) }),
700
+ /* @__PURE__ */ jsx5(Row, { label: "Muted", name: "muted", children: /* @__PURE__ */ jsx5("input", { type: "checkbox", className: "justify-self-start", checked: !!clip.muted, disabled, "aria-label": "Muted", onChange: (e) => set("muted", e.target.checked || void 0, e.target.checked ? "Mute clip" : "Unmute clip") }) }),
612
701
  ["fadeInCurve", "fadeOutCurve"].map(
613
- (key) => (key === "fadeInCurve" ? clip.fadeIn : clip.fadeOut) ? /* @__PURE__ */ jsx4(Row, { label: key === "fadeInCurve" ? "Fade in sound" : "Fade out sound", name: key, children: /* @__PURE__ */ jsx4(
702
+ (key) => (key === "fadeInCurve" ? clip.fadeIn : clip.fadeOut) ? /* @__PURE__ */ jsx5(Row, { label: key === "fadeInCurve" ? "Fade in sound" : "Fade out sound", name: key, children: /* @__PURE__ */ jsx5(
614
703
  "select",
615
704
  {
616
705
  className: field,
@@ -621,19 +710,19 @@ function ClipInspector({ controller, className = "" }) {
621
710
  const curve = e.target.value;
622
711
  set(key, curve === "linear" ? void 0 : curve, key === "fadeInCurve" ? "Change fade in curve" : "Change fade out curve");
623
712
  },
624
- children: VIDEO_FADE_CURVES.map((c) => /* @__PURE__ */ jsx4("option", { value: c, children: VIDEO_FADE_CURVE_LABELS[c] }, c))
713
+ children: VIDEO_FADE_CURVES.map((c) => /* @__PURE__ */ jsx5("option", { value: c, children: VIDEO_FADE_CURVE_LABELS[c] }, c))
625
714
  }
626
715
  ) }, key) : null
627
716
  )
628
717
  ] }),
629
- clip.type === "text" && /* @__PURE__ */ jsxs3(Fragment3, { children: [
630
- /* @__PURE__ */ jsx4(Row, { label: "Text", name: "text", children: /* @__PURE__ */ jsx4(Commit, { multiline: true, value: clip.text, "aria-label": "Text", disabled, onCommit: (raw) => set("text", raw, "Change text") }) }),
631
- /* @__PURE__ */ jsx4(Row, { label: "Colour", name: "color", children: text("color", "Colour") }),
632
- /* @__PURE__ */ jsx4(Row, { label: "Font size", name: "fontSize", children: number("fontSize", "Font size", { min: 1, step: 2 }) }),
633
- /* @__PURE__ */ jsx4(Row, { label: "Background", name: "background", children: text("background", "Background") })
718
+ clip.type === "text" && /* @__PURE__ */ jsxs4(Fragment3, { children: [
719
+ /* @__PURE__ */ jsx5(Row, { label: "Text", name: "text", children: /* @__PURE__ */ jsx5(Commit, { multiline: true, value: clip.text, "aria-label": "Text", disabled, onCommit: (raw) => set("text", raw, "Change text") }) }),
720
+ /* @__PURE__ */ jsx5(Row, { label: "Colour", name: "color", children: text("color", "Colour") }),
721
+ /* @__PURE__ */ jsx5(Row, { label: "Font size", name: "fontSize", children: number("fontSize", "Font size", { min: 1, step: 2 }) }),
722
+ /* @__PURE__ */ jsx5(Row, { label: "Background", name: "background", children: text("background", "Background") })
634
723
  ] }),
635
- clip.type === "color" && /* @__PURE__ */ jsx4(Row, { label: "Colour", name: "color", children: /* @__PURE__ */ jsx4(Commit, { value: clip.color, "aria-label": "Colour", disabled, onCommit: (raw) => raw.trim() && set("color", raw.trim(), "Change colour") }) }),
636
- /* @__PURE__ */ jsx4(Row, { label: "Transition in", name: "transition", children: /* @__PURE__ */ jsxs3(
724
+ clip.type === "color" && /* @__PURE__ */ jsx5(Row, { label: "Colour", name: "color", children: /* @__PURE__ */ jsx5(Commit, { value: clip.color, "aria-label": "Colour", disabled, onCommit: (raw) => raw.trim() && set("color", raw.trim(), "Change colour") }) }),
725
+ /* @__PURE__ */ jsx5(Row, { label: "Transition in", name: "transition", children: /* @__PURE__ */ jsxs4(
637
726
  "select",
638
727
  {
639
728
  className: field,
@@ -648,12 +737,12 @@ function ClipInspector({ controller, className = "" }) {
648
737
  );
649
738
  },
650
739
  children: [
651
- /* @__PURE__ */ jsx4("option", { value: "", children: "None" }),
652
- VIDEO_TRANSITION_TYPES.map((t) => /* @__PURE__ */ jsx4("option", { value: t, children: VIDEO_TRANSITION_LABELS[t] }, t))
740
+ /* @__PURE__ */ jsx5("option", { value: "", children: "None" }),
741
+ VIDEO_TRANSITION_TYPES.map((t) => /* @__PURE__ */ jsx5("option", { value: t, children: VIDEO_TRANSITION_LABELS[t] }, t))
653
742
  ]
654
743
  }
655
744
  ) }),
656
- transition && /* @__PURE__ */ jsx4(Row, { label: "Length (ms)", name: "transitionDuration", children: /* @__PURE__ */ jsx4(
745
+ transition && /* @__PURE__ */ jsx5(Row, { label: "Length (ms)", name: "transitionDuration", children: /* @__PURE__ */ jsx5(
657
746
  Commit,
658
747
  {
659
748
  type: "number",
@@ -668,7 +757,7 @@ function ClipInspector({ controller, className = "" }) {
668
757
  }
669
758
  }
670
759
  ) }),
671
- transition && videoTransitionHasDirection(transition.type) && /* @__PURE__ */ jsx4(Row, { label: "Direction", name: "transitionDirection", children: /* @__PURE__ */ jsx4(
760
+ transition && videoTransitionHasDirection(transition.type) && /* @__PURE__ */ jsx5(Row, { label: "Direction", name: "transitionDirection", children: /* @__PURE__ */ jsx5(
672
761
  "select",
673
762
  {
674
763
  className: field,
@@ -676,10 +765,10 @@ function ClipInspector({ controller, className = "" }) {
676
765
  disabled,
677
766
  "aria-label": "Transition direction",
678
767
  onChange: (e) => controller.apply({ op: "set_transition", clip: clip.id, transition: { ...transition, direction: e.target.value } }, "Change transition direction"),
679
- children: VIDEO_WIPE_DIRECTIONS.map((d) => /* @__PURE__ */ jsx4("option", { value: d, children: d }, d))
768
+ children: VIDEO_WIPE_DIRECTIONS.map((d) => /* @__PURE__ */ jsx5("option", { value: d, children: d }, d))
680
769
  }
681
770
  ) }),
682
- transition && /* @__PURE__ */ jsx4(Row, { label: "Transition sound", name: "transitionAudioCurve", children: /* @__PURE__ */ jsx4(
771
+ transition && /* @__PURE__ */ jsx5(Row, { label: "Transition sound", name: "transitionAudioCurve", children: /* @__PURE__ */ jsx5(
683
772
  "select",
684
773
  {
685
774
  className: field,
@@ -692,30 +781,30 @@ function ClipInspector({ controller, className = "" }) {
692
781
  const next = curve === videoTransitionAudioCurve(rest) ? rest : { ...rest, audioCurve: curve };
693
782
  controller.apply({ op: "set_transition", clip: clip.id, transition: next }, "Change transition sound");
694
783
  },
695
- children: VIDEO_FADE_CURVES.map((c) => /* @__PURE__ */ jsx4("option", { value: c, children: VIDEO_FADE_CURVE_LABELS[c] }, c))
784
+ children: VIDEO_FADE_CURVES.map((c) => /* @__PURE__ */ jsx5("option", { value: c, children: VIDEO_FADE_CURVE_LABELS[c] }, c))
696
785
  }
697
786
  ) })
698
787
  ] });
699
788
  }
700
789
 
701
790
  // src/video-editor/MediaBin.tsx
702
- import { useState as useState2 } from "react";
791
+ import { useState as useState3 } from "react";
703
792
  import {
704
793
  formatVideoSeconds as formatVideoSeconds3,
705
794
  probeVideoMedia,
706
795
  videoAssetDropEdit,
707
796
  videoAssetKindOf
708
797
  } from "@pixerate/editor/video-editor";
709
- import { Fragment as Fragment4, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
710
- var btn2 = "rounded border border-border px-2 py-0.5 text-xs disabled:opacity-40";
798
+ import { Fragment as Fragment4, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
799
+ var btn3 = "rounded border border-border px-2 py-0.5 text-xs disabled:opacity-40";
711
800
  function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMedia, className = "" }) {
712
801
  const { project, readOnly } = useVideoEditorState(controller);
713
802
  const canEdit = editable && !readOnly;
714
- const [url, setUrl] = useState2("");
715
- const [urlKind, setUrlKind] = useState2("");
716
- const [busy, setBusy] = useState2(0);
717
- const [problem, setProblem] = useState2(null);
718
- const [over, setOver] = useState2(false);
803
+ const [url, setUrl] = useState3("");
804
+ const [urlKind, setUrlKind] = useState3("");
805
+ const [busy, setBusy] = useState3(0);
806
+ const [problem, setProblem] = useState3(null);
807
+ const [over, setOver] = useState3(false);
719
808
  const addSource = async (src, kind, name) => {
720
809
  setBusy((n) => n + 1);
721
810
  try {
@@ -763,7 +852,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMe
763
852
  if (added) controller.select([added]);
764
853
  }
765
854
  };
766
- return /* @__PURE__ */ jsxs4(
855
+ return /* @__PURE__ */ jsxs5(
767
856
  "div",
768
857
  {
769
858
  className: `flex flex-col gap-2 rounded ${over ? "ring-2 ring-rose-500/60" : ""} ${className}`,
@@ -781,20 +870,20 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMe
781
870
  void addFiles(e.dataTransfer.files);
782
871
  },
783
872
  children: [
784
- /* @__PURE__ */ jsx5("div", { className: "text-xs font-medium", children: "Media" }),
785
- project.assets.length ? /* @__PURE__ */ jsx5("ul", { className: "flex flex-col gap-1", "aria-label": "Media assets", children: project.assets.map((asset) => /* @__PURE__ */ jsxs4("li", { className: "flex items-center gap-2 text-xs", "data-asset": asset.id, children: [
786
- /* @__PURE__ */ jsx5("span", { className: "rounded bg-muted px-1 text-[10px] uppercase text-muted-foreground", "data-asset-kind": "", children: asset.kind }),
787
- /* @__PURE__ */ jsx5("span", { className: "min-w-0 flex-1 truncate", title: asset.src, children: asset.name ?? asset.id }),
788
- asset.duration !== void 0 && /* @__PURE__ */ jsx5("span", { className: "tabular-nums text-muted-foreground", children: formatVideoSeconds3(asset.duration) }),
789
- editable && /* @__PURE__ */ jsx5("button", { type: "button", className: btn2, disabled: !canEdit, onClick: () => addToTimeline(asset), "data-add-to-timeline": "", children: "Add to timeline" })
790
- ] }, asset.id)) }) : /* @__PURE__ */ jsxs4("p", { className: "text-xs text-muted-foreground", children: [
873
+ /* @__PURE__ */ jsx6("div", { className: "text-xs font-medium", children: "Media" }),
874
+ project.assets.length ? /* @__PURE__ */ jsx6("ul", { className: "flex flex-col gap-1", "aria-label": "Media assets", children: project.assets.map((asset) => /* @__PURE__ */ jsxs5("li", { className: "flex items-center gap-2 text-xs", "data-asset": asset.id, children: [
875
+ /* @__PURE__ */ jsx6("span", { className: "rounded bg-muted px-1 text-[10px] uppercase text-muted-foreground", "data-asset-kind": "", children: asset.kind }),
876
+ /* @__PURE__ */ jsx6("span", { className: "min-w-0 flex-1 truncate", title: asset.src, children: asset.name ?? asset.id }),
877
+ asset.duration !== void 0 && /* @__PURE__ */ jsx6("span", { className: "tabular-nums text-muted-foreground", children: formatVideoSeconds3(asset.duration) }),
878
+ editable && /* @__PURE__ */ jsx6("button", { type: "button", className: btn3, disabled: !canEdit, onClick: () => addToTimeline(asset), "data-add-to-timeline": "", children: "Add to timeline" })
879
+ ] }, asset.id)) }) : /* @__PURE__ */ jsxs5("p", { className: "text-xs text-muted-foreground", children: [
791
880
  "No media yet.",
792
881
  editable ? " Choose or drop files, or add a URL." : ""
793
882
  ] }),
794
- editable && /* @__PURE__ */ jsxs4(Fragment4, { children: [
795
- /* @__PURE__ */ jsxs4("label", { className: "flex flex-col gap-1 rounded border border-dashed border-border p-2 text-xs text-muted-foreground", children: [
883
+ editable && /* @__PURE__ */ jsxs5(Fragment4, { children: [
884
+ /* @__PURE__ */ jsxs5("label", { className: "flex flex-col gap-1 rounded border border-dashed border-border p-2 text-xs text-muted-foreground", children: [
796
885
  "Choose or drop video, audio or image files",
797
- /* @__PURE__ */ jsx5(
886
+ /* @__PURE__ */ jsx6(
798
887
  "input",
799
888
  {
800
889
  type: "file",
@@ -811,7 +900,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMe
811
900
  }
812
901
  )
813
902
  ] }),
814
- /* @__PURE__ */ jsxs4(
903
+ /* @__PURE__ */ jsxs5(
815
904
  "form",
816
905
  {
817
906
  className: "flex items-center gap-1",
@@ -820,7 +909,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMe
820
909
  void addUrl();
821
910
  },
822
911
  children: [
823
- /* @__PURE__ */ jsx5(
912
+ /* @__PURE__ */ jsx6(
824
913
  "input",
825
914
  {
826
915
  className: "min-w-0 flex-1 rounded border border-border bg-transparent px-1 py-0.5 text-xs",
@@ -831,26 +920,26 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMe
831
920
  onChange: (e) => setUrl(e.target.value)
832
921
  }
833
922
  ),
834
- /* @__PURE__ */ jsxs4("select", { className: "rounded border border-border bg-transparent px-1 py-0.5 text-xs", value: urlKind, disabled: !canEdit, "aria-label": "Media kind", onChange: (e) => setUrlKind(e.target.value), children: [
835
- /* @__PURE__ */ jsx5("option", { value: "", children: "Auto" }),
836
- /* @__PURE__ */ jsx5("option", { value: "video", children: "Video" }),
837
- /* @__PURE__ */ jsx5("option", { value: "audio", children: "Audio" }),
838
- /* @__PURE__ */ jsx5("option", { value: "image", children: "Image" })
923
+ /* @__PURE__ */ jsxs5("select", { className: "rounded border border-border bg-transparent px-1 py-0.5 text-xs", value: urlKind, disabled: !canEdit, "aria-label": "Media kind", onChange: (e) => setUrlKind(e.target.value), children: [
924
+ /* @__PURE__ */ jsx6("option", { value: "", children: "Auto" }),
925
+ /* @__PURE__ */ jsx6("option", { value: "video", children: "Video" }),
926
+ /* @__PURE__ */ jsx6("option", { value: "audio", children: "Audio" }),
927
+ /* @__PURE__ */ jsx6("option", { value: "image", children: "Image" })
839
928
  ] }),
840
- /* @__PURE__ */ jsx5("button", { type: "submit", className: btn2, disabled: !canEdit || !url.trim(), "data-add-url": "", children: "Add" })
929
+ /* @__PURE__ */ jsx6("button", { type: "submit", className: btn3, disabled: !canEdit || !url.trim(), "data-add-url": "", children: "Add" })
841
930
  ]
842
931
  }
843
932
  )
844
933
  ] }),
845
- busy > 0 && /* @__PURE__ */ jsx5("p", { className: "text-xs text-muted-foreground", children: "Reading media\u2026" }),
846
- problem && /* @__PURE__ */ jsx5("p", { className: "text-xs text-red-500", role: "alert", "data-media-error": "", children: problem })
934
+ busy > 0 && /* @__PURE__ */ jsx6("p", { className: "text-xs text-muted-foreground", children: "Reading media\u2026" }),
935
+ problem && /* @__PURE__ */ jsx6("p", { className: "text-xs text-red-500", role: "alert", "data-media-error": "", children: problem })
847
936
  ]
848
937
  }
849
938
  );
850
939
  }
851
940
 
852
941
  // src/video-editor/TranscriptPanel.tsx
853
- import React4, { useMemo as useMemo3, useState as useState3 } from "react";
942
+ import React5, { useMemo as useMemo3, useState as useState4 } from "react";
854
943
  import {
855
944
  findVideoFillers,
856
945
  findVideoTranscriptPauses,
@@ -860,15 +949,15 @@ import {
860
949
  videoWordAt,
861
950
  videoWordsRange
862
951
  } from "@pixerate/editor/video-editor";
863
- import { Fragment as Fragment5, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
864
- var btn3 = "rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40";
952
+ import { Fragment as Fragment5, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
953
+ var btn4 = "rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40";
865
954
  function TranscriptPanel({ controller, track, editable = true, className = "" }) {
866
955
  const state = useVideoEditorState(controller);
867
956
  const { project, time, readOnly } = state;
868
957
  const canEdit = editable && !readOnly;
869
- const [anchor, setAnchor] = useState3(null);
870
- const [focus, setFocus] = useState3(null);
871
- const [message, setMessage] = useState3(null);
958
+ const [anchor, setAnchor] = useState4(null);
959
+ const [focus, setFocus] = useState4(null);
960
+ const [message, setMessage] = useState4(null);
872
961
  const words = useMemo3(() => {
873
962
  try {
874
963
  return videoTimelineTranscript(project, { track });
@@ -895,9 +984,9 @@ function TranscriptPanel({ controller, track, editable = true, className = "" })
895
984
  setMessage(e instanceof Error ? e.message : String(e));
896
985
  }
897
986
  };
898
- return /* @__PURE__ */ jsxs5("div", { className: `flex flex-col gap-2 text-xs ${className}`, "data-transcript-panel": "", children: [
899
- words.length > 0 ? /* @__PURE__ */ jsxs5(Fragment5, { children: [
900
- /* @__PURE__ */ jsx6(
987
+ return /* @__PURE__ */ jsxs6("div", { className: `flex flex-col gap-2 text-xs ${className}`, "data-transcript-panel": "", children: [
988
+ words.length > 0 ? /* @__PURE__ */ jsxs6(Fragment5, { children: [
989
+ /* @__PURE__ */ jsx7(
901
990
  "div",
902
991
  {
903
992
  className: "max-h-48 overflow-y-auto leading-6",
@@ -913,8 +1002,8 @@ function TranscriptPanel({ controller, track, editable = true, className = "" })
913
1002
  },
914
1003
  children: words.map((w, i) => {
915
1004
  const inSelection = selected !== null && i >= selected[0] && i <= selected[1];
916
- return /* @__PURE__ */ jsxs5(React4.Fragment, { children: [
917
- /* @__PURE__ */ jsx6(
1005
+ return /* @__PURE__ */ jsxs6(React5.Fragment, { children: [
1006
+ /* @__PURE__ */ jsx7(
918
1007
  "span",
919
1008
  {
920
1009
  role: "option",
@@ -940,51 +1029,52 @@ function TranscriptPanel({ controller, track, editable = true, className = "" })
940
1029
  })
941
1030
  }
942
1031
  ),
943
- canEdit && /* @__PURE__ */ jsxs5("div", { className: "flex flex-wrap gap-1", children: [
944
- /* @__PURE__ */ jsx6("button", { type: "button", className: btn3, disabled: !selected, onClick: () => selected && cut([videoWordsRange(words, selected[0], selected[1])], "Cut words", "Select words to cut."), children: "Delete selected words" }),
945
- /* @__PURE__ */ jsx6("button", { type: "button", className: btn3, onClick: () => cut(findVideoFillers(words), "Remove fillers", "No filler words found."), children: "Remove fillers" }),
946
- /* @__PURE__ */ jsx6("button", { type: "button", className: btn3, onClick: () => cut(findVideoTranscriptPauses(words, { fps: project.fps }), "Tighten pauses", "No long pauses found."), children: "Tighten pauses" })
1032
+ canEdit && /* @__PURE__ */ jsxs6("div", { className: "flex flex-wrap gap-1", children: [
1033
+ /* @__PURE__ */ jsx7("button", { type: "button", className: btn4, disabled: !selected, onClick: () => selected && cut([videoWordsRange(words, selected[0], selected[1])], "Cut words", "Select words to cut."), children: "Delete selected words" }),
1034
+ /* @__PURE__ */ jsx7("button", { type: "button", className: btn4, onClick: () => cut(findVideoFillers(words), "Remove fillers", "No filler words found."), children: "Remove fillers" }),
1035
+ /* @__PURE__ */ jsx7("button", { type: "button", className: btn4, onClick: () => cut(findVideoTranscriptPauses(words, { fps: project.fps }), "Tighten pauses", "No long pauses found."), children: "Tighten pauses" })
947
1036
  ] })
948
- ] }) : /* @__PURE__ */ jsx6("p", { className: "text-muted-foreground", "data-transcript-empty": "", children: "No transcribed media on the timeline. Load a transcript (Whisper JSON, .srt or .vtt) for a video or audio asset to edit by text." }),
949
- canEdit && sound.length > 0 && /* @__PURE__ */ jsxs5("details", { children: [
950
- /* @__PURE__ */ jsx6("summary", { className: "cursor-pointer text-muted-foreground", children: "Transcripts" }),
951
- /* @__PURE__ */ jsx6("ul", { className: "mt-1 space-y-1", children: sound.map((a) => /* @__PURE__ */ jsxs5("li", { className: "flex items-center gap-2", children: [
952
- /* @__PURE__ */ jsxs5("span", { className: "min-w-0 flex-1 truncate", children: [
1037
+ ] }) : /* @__PURE__ */ jsx7("p", { className: "text-muted-foreground", "data-transcript-empty": "", children: "No transcribed media on the timeline. Load a transcript (Whisper JSON, .srt or .vtt) for a video or audio asset to edit by text." }),
1038
+ canEdit && sound.length > 0 && /* @__PURE__ */ jsxs6("details", { children: [
1039
+ /* @__PURE__ */ jsx7("summary", { className: "cursor-pointer text-muted-foreground", children: "Transcripts" }),
1040
+ /* @__PURE__ */ jsx7("ul", { className: "mt-1 space-y-1", children: sound.map((a) => /* @__PURE__ */ jsxs6("li", { className: "flex items-center gap-2", children: [
1041
+ /* @__PURE__ */ jsxs6("span", { className: "min-w-0 flex-1 truncate", children: [
953
1042
  a.name ?? a.id,
954
1043
  " ",
955
1044
  a.transcript ? `\xB7 ${a.transcript.length} words` : ""
956
1045
  ] }),
957
- /* @__PURE__ */ jsx6("input", { type: "file", accept: ".json,.srt,.vtt,application/json,text/vtt", "aria-label": `Load a transcript for ${a.name ?? a.id}`, className: "w-40 text-[10px]", onChange: (e) => void load(a.id, e.target.files?.[0]) })
1046
+ /* @__PURE__ */ jsx7("input", { type: "file", accept: ".json,.srt,.vtt,application/json,text/vtt", "aria-label": `Load a transcript for ${a.name ?? a.id}`, className: "w-40 text-[10px]", onChange: (e) => void load(a.id, e.target.files?.[0]) })
958
1047
  ] }, a.id)) })
959
1048
  ] }),
960
- message && /* @__PURE__ */ jsx6("p", { className: "text-red-500", role: "alert", "data-transcript-error": "", children: message })
1049
+ message && /* @__PURE__ */ jsx7("p", { className: "text-red-500", role: "alert", "data-transcript-error": "", children: message })
961
1050
  ] });
962
1051
  }
963
1052
 
964
1053
  // src/video-editor/VideoEditor.tsx
965
- import { useEffect as useEffect4 } from "react";
966
- import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
967
- function VideoEditor({ controller: given, options, editable = true, pxPerSecond, onPxPerSecondChange, resolution, showIssues = true, showTranscript = true, className = "" }) {
1054
+ import { useEffect as useEffect5 } from "react";
1055
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1056
+ function VideoEditor({ controller: given, options, editable = true, pxPerSecond, onPxPerSecondChange, resolution, showIssues = true, showTranscript = true, showScopes = false, className = "" }) {
968
1057
  const { controller, state } = useVideoEditor(given ?? options ?? {});
969
- useEffect4(() => {
1058
+ useEffect5(() => {
970
1059
  if (editable) return;
971
1060
  const was = controller.getState().readOnly;
972
1061
  controller.setReadOnly(true);
973
1062
  return () => controller.setReadOnly(was);
974
1063
  }, [controller, editable]);
975
1064
  const { issues } = state;
976
- const issueList = showIssues && issues.length > 0 && /* @__PURE__ */ jsx7("ul", { className: "space-y-0.5 text-xs", "aria-label": "Video issues", "data-video-issues": "", children: issues.map((issue, i) => /* @__PURE__ */ jsx7("li", { className: issue.severity === "error" ? "text-red-500" : issue.severity === "warning" ? "text-amber-500" : "text-muted-foreground", "data-issue": issue.code, children: issue.message }, i)) });
977
- return /* @__PURE__ */ jsxs6("div", { className: `flex flex-col gap-3 ${className}`, "data-video-editor": "", children: [
978
- /* @__PURE__ */ jsxs6("div", { className: `grid gap-3 ${editable ? "lg:grid-cols-[16rem_minmax(0,1fr)_18rem]" : ""}`, children: [
979
- editable && /* @__PURE__ */ jsx7(MediaBin, { controller, className: "min-w-0" }),
980
- /* @__PURE__ */ jsxs6("div", { className: "flex min-w-0 flex-col gap-2", children: [
981
- /* @__PURE__ */ jsx7(VideoPreview, { controller, resolution, className: "rounded-md border border-border" }),
982
- /* @__PURE__ */ jsx7(VideoTransport, { controller, editable })
1065
+ const issueList = showIssues && issues.length > 0 && /* @__PURE__ */ jsx8("ul", { className: "space-y-0.5 text-xs", "aria-label": "Video issues", "data-video-issues": "", children: issues.map((issue, i) => /* @__PURE__ */ jsx8("li", { className: issue.severity === "error" ? "text-red-500" : issue.severity === "warning" ? "text-amber-500" : "text-muted-foreground", "data-issue": issue.code, children: issue.message }, i)) });
1066
+ return /* @__PURE__ */ jsxs7("div", { className: `flex flex-col gap-3 ${className}`, "data-video-editor": "", children: [
1067
+ /* @__PURE__ */ jsxs7("div", { className: `grid gap-3 ${editable ? "lg:grid-cols-[16rem_minmax(0,1fr)_18rem]" : ""}`, children: [
1068
+ editable && /* @__PURE__ */ jsx8(MediaBin, { controller, className: "min-w-0" }),
1069
+ /* @__PURE__ */ jsxs7("div", { className: "flex min-w-0 flex-col gap-2", children: [
1070
+ /* @__PURE__ */ jsx8(VideoPreview, { controller, resolution, className: "rounded-md border border-border" }),
1071
+ /* @__PURE__ */ jsx8(VideoTransport, { controller, editable }),
1072
+ showScopes && /* @__PURE__ */ jsx8(VideoScopes, { controller })
983
1073
  ] }),
984
- editable && /* @__PURE__ */ jsx7(ClipInspector, { controller, className: "min-w-0" })
1074
+ editable && /* @__PURE__ */ jsx8(ClipInspector, { controller, className: "min-w-0" })
985
1075
  ] }),
986
- /* @__PURE__ */ jsx7(VideoTimeline, { controller, editable, pxPerSecond, onPxPerSecondChange }),
987
- showTranscript && /* @__PURE__ */ jsx7(TranscriptPanel, { controller, editable }),
1076
+ /* @__PURE__ */ jsx8(VideoTimeline, { controller, editable, pxPerSecond, onPxPerSecondChange }),
1077
+ showTranscript && /* @__PURE__ */ jsx8(TranscriptPanel, { controller, editable }),
988
1078
  issueList
989
1079
  ] });
990
1080
  }
@@ -994,6 +1084,7 @@ export {
994
1084
  TranscriptPanel,
995
1085
  VideoEditor,
996
1086
  VideoPreview,
1087
+ VideoScopes,
997
1088
  VideoTimeline,
998
1089
  VideoTransport,
999
1090
  useVideoEditor,