@pixerate/editor-react 0.56.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
 
@@ -442,7 +510,7 @@ import {
442
510
  videoKeyframeToggleEdit,
443
511
  videoKeyframeValueEdit
444
512
  } from "@pixerate/editor/video-editor";
445
- 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";
446
514
  var btn2 = "rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40";
447
515
  var field = "w-full rounded border border-border bg-transparent px-1 py-0.5 text-xs disabled:opacity-50";
448
516
  var DEFAULT_TRANSITION = 500;
@@ -456,7 +524,7 @@ function Commit({
456
524
  if (raw !== value) onCommit(raw);
457
525
  };
458
526
  if (multiline)
459
- return /* @__PURE__ */ jsx4(
527
+ return /* @__PURE__ */ jsx5(
460
528
  "textarea",
461
529
  {
462
530
  className: field,
@@ -468,11 +536,11 @@ function Commit({
468
536
  },
469
537
  value
470
538
  );
471
- 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);
472
540
  }
473
541
  function Row({ label, name, children }) {
474
- return /* @__PURE__ */ jsxs3("label", { className: "grid grid-cols-[5.5rem_minmax(0,1fr)] items-center gap-2 text-xs", "data-field": name, children: [
475
- /* @__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 }),
476
544
  children
477
545
  ] });
478
546
  }
@@ -480,7 +548,7 @@ function ClipInspector({ controller, className = "" }) {
480
548
  const state = useVideoEditorState(controller);
481
549
  const { project, selection, readOnly, time } = state;
482
550
  if (selection.length !== 1)
483
- 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." });
484
552
  const at = findVideoClip(project, selection[0]);
485
553
  if (!at) return null;
486
554
  const clip = at.clip;
@@ -497,24 +565,24 @@ function ClipInspector({ controller, className = "" }) {
497
565
  };
498
566
  const text = (key, label) => {
499
567
  const value = clip[key];
500
- 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}`) });
501
569
  };
502
570
  const number = (key, label, attrs = {}) => {
503
571
  const value = clip[key];
504
- 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 });
505
573
  };
506
574
  const transition = clip.transition;
507
- return /* @__PURE__ */ jsxs3("div", { className: `flex flex-col gap-1.5 ${className}`, "data-clip-inspector": "", "data-clip-id": clip.id, children: [
508
- /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2 text-xs", children: [
509
- /* @__PURE__ */ jsx4("span", { className: "font-medium", children: clip.type === "media" ? "Media clip" : clip.type === "text" ? "Title" : "Colour clip" }),
510
- /* @__PURE__ */ jsx4("code", { className: "text-muted-foreground", children: clip.id }),
511
- 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" })
512
580
  ] }),
513
- /* @__PURE__ */ jsx4(Row, { label: "Name", name: "name", children: text("name", "Name") }),
514
- /* @__PURE__ */ jsx4(Row, { label: "Start", name: "start", children: /* @__PURE__ */ jsx4("span", { className: "tabular-nums text-xs", children: formatVideoSeconds2(clip.start) }) }),
515
- /* @__PURE__ */ jsx4(Row, { label: "Duration", name: "duration", children: /* @__PURE__ */ jsx4("span", { className: "tabular-nums text-xs", children: formatVideoSeconds2(clip.duration) }) }),
516
- 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: [
517
- /* @__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(
518
586
  Commit,
519
587
  {
520
588
  type: "number",
@@ -533,7 +601,7 @@ function ClipInspector({ controller, className = "" }) {
533
601
  }
534
602
  }
535
603
  ),
536
- /* @__PURE__ */ jsx4(
604
+ /* @__PURE__ */ jsx5(
537
605
  "button",
538
606
  {
539
607
  type: "button",
@@ -550,11 +618,11 @@ function ClipInspector({ controller, className = "" }) {
550
618
  children: row.onKey ? "\u25C6" : "\u25C7"
551
619
  }
552
620
  ),
553
- 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 })
554
622
  ] }) }, row.property)),
555
- 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)) }) }),
556
- at.track.kind === "video" && /* @__PURE__ */ jsxs3(Fragment3, { children: [
557
- /* @__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(
558
626
  "select",
559
627
  {
560
628
  className: field,
@@ -566,12 +634,12 @@ function ClipInspector({ controller, className = "" }) {
566
634
  if (grade) controller.apply({ op: "update_clip", clip: clip.id, set: { grade } }, `Apply ${e.target.value} look`);
567
635
  },
568
636
  children: [
569
- /* @__PURE__ */ jsx4("option", { value: "", children: clip.grade ? "Graded" : "None" }),
570
- 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))
571
639
  ]
572
640
  }
573
641
  ) }),
574
- 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(
575
643
  Commit,
576
644
  {
577
645
  type: "number",
@@ -591,7 +659,7 @@ function ClipInspector({ controller, className = "" }) {
591
659
  }
592
660
  ) }, c.key))
593
661
  ] }),
594
- 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(
595
663
  "select",
596
664
  {
597
665
  className: field,
@@ -603,20 +671,20 @@ function ClipInspector({ controller, className = "" }) {
603
671
  set(key, effect ? { ...clip[key], effect } : void 0, key === "animateIn" ? "Change entrance" : "Change exit");
604
672
  },
605
673
  children: [
606
- /* @__PURE__ */ jsx4("option", { value: "", children: "None" }),
607
- 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))
608
676
  ]
609
677
  }
610
678
  ) }, key)),
611
- /* @__PURE__ */ jsx4(Row, { label: "Fade in (ms)", name: "fadeIn", children: number("fadeIn", "Fade in", { min: 0, step: 100 }) }),
612
- /* @__PURE__ */ jsx4(Row, { label: "Fade out (ms)", name: "fadeOut", children: number("fadeOut", "Fade out", { min: 0, step: 100 }) }),
613
- clip.type === "media" && /* @__PURE__ */ jsxs3(Fragment3, { children: [
614
- /* @__PURE__ */ jsx4(Row, { label: "Speed", name: "speed", children: clip.timeRemap ? /* @__PURE__ */ jsxs3("span", { className: "flex items-center gap-1", "data-ramped": "", children: [
615
- /* @__PURE__ */ jsx4("span", { className: "text-muted-foreground", children: "ramped" }),
616
- /* @__PURE__ */ jsx4("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" })
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" })
617
685
  ] }) : number("speed", "Speed", { min: 0.1, step: 0.25 }) }),
618
- project.assets.find((a) => a.id === clip.asset)?.kind !== "image" && /* @__PURE__ */ jsx4(Row, { label: "Retime", name: "retime", children: /* @__PURE__ */ jsxs3("span", { className: "flex flex-wrap gap-1", children: [
619
- /* @__PURE__ */ jsx4(
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(
620
688
  "button",
621
689
  {
622
690
  type: "button",
@@ -627,11 +695,11 @@ function ClipInspector({ controller, className = "" }) {
627
695
  children: "Freeze 1 s here"
628
696
  }
629
697
  ),
630
- /* @__PURE__ */ jsx4("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" })
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" })
631
699
  ] }) }),
632
- /* @__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") }) }),
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") }) }),
633
701
  ["fadeInCurve", "fadeOutCurve"].map(
634
- (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(
635
703
  "select",
636
704
  {
637
705
  className: field,
@@ -642,19 +710,19 @@ function ClipInspector({ controller, className = "" }) {
642
710
  const curve = e.target.value;
643
711
  set(key, curve === "linear" ? void 0 : curve, key === "fadeInCurve" ? "Change fade in curve" : "Change fade out curve");
644
712
  },
645
- 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))
646
714
  }
647
715
  ) }, key) : null
648
716
  )
649
717
  ] }),
650
- clip.type === "text" && /* @__PURE__ */ jsxs3(Fragment3, { children: [
651
- /* @__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") }) }),
652
- /* @__PURE__ */ jsx4(Row, { label: "Colour", name: "color", children: text("color", "Colour") }),
653
- /* @__PURE__ */ jsx4(Row, { label: "Font size", name: "fontSize", children: number("fontSize", "Font size", { min: 1, step: 2 }) }),
654
- /* @__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") })
655
723
  ] }),
656
- 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") }) }),
657
- /* @__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(
658
726
  "select",
659
727
  {
660
728
  className: field,
@@ -669,12 +737,12 @@ function ClipInspector({ controller, className = "" }) {
669
737
  );
670
738
  },
671
739
  children: [
672
- /* @__PURE__ */ jsx4("option", { value: "", children: "None" }),
673
- 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))
674
742
  ]
675
743
  }
676
744
  ) }),
677
- 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(
678
746
  Commit,
679
747
  {
680
748
  type: "number",
@@ -689,7 +757,7 @@ function ClipInspector({ controller, className = "" }) {
689
757
  }
690
758
  }
691
759
  ) }),
692
- 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(
693
761
  "select",
694
762
  {
695
763
  className: field,
@@ -697,10 +765,10 @@ function ClipInspector({ controller, className = "" }) {
697
765
  disabled,
698
766
  "aria-label": "Transition direction",
699
767
  onChange: (e) => controller.apply({ op: "set_transition", clip: clip.id, transition: { ...transition, direction: e.target.value } }, "Change transition direction"),
700
- 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))
701
769
  }
702
770
  ) }),
703
- 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(
704
772
  "select",
705
773
  {
706
774
  className: field,
@@ -713,30 +781,30 @@ function ClipInspector({ controller, className = "" }) {
713
781
  const next = curve === videoTransitionAudioCurve(rest) ? rest : { ...rest, audioCurve: curve };
714
782
  controller.apply({ op: "set_transition", clip: clip.id, transition: next }, "Change transition sound");
715
783
  },
716
- 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))
717
785
  }
718
786
  ) })
719
787
  ] });
720
788
  }
721
789
 
722
790
  // src/video-editor/MediaBin.tsx
723
- import { useState as useState2 } from "react";
791
+ import { useState as useState3 } from "react";
724
792
  import {
725
793
  formatVideoSeconds as formatVideoSeconds3,
726
794
  probeVideoMedia,
727
795
  videoAssetDropEdit,
728
796
  videoAssetKindOf
729
797
  } from "@pixerate/editor/video-editor";
730
- import { Fragment as Fragment4, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
798
+ import { Fragment as Fragment4, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
731
799
  var btn3 = "rounded border border-border px-2 py-0.5 text-xs disabled:opacity-40";
732
800
  function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMedia, className = "" }) {
733
801
  const { project, readOnly } = useVideoEditorState(controller);
734
802
  const canEdit = editable && !readOnly;
735
- const [url, setUrl] = useState2("");
736
- const [urlKind, setUrlKind] = useState2("");
737
- const [busy, setBusy] = useState2(0);
738
- const [problem, setProblem] = useState2(null);
739
- 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);
740
808
  const addSource = async (src, kind, name) => {
741
809
  setBusy((n) => n + 1);
742
810
  try {
@@ -784,7 +852,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMe
784
852
  if (added) controller.select([added]);
785
853
  }
786
854
  };
787
- return /* @__PURE__ */ jsxs4(
855
+ return /* @__PURE__ */ jsxs5(
788
856
  "div",
789
857
  {
790
858
  className: `flex flex-col gap-2 rounded ${over ? "ring-2 ring-rose-500/60" : ""} ${className}`,
@@ -802,20 +870,20 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMe
802
870
  void addFiles(e.dataTransfer.files);
803
871
  },
804
872
  children: [
805
- /* @__PURE__ */ jsx5("div", { className: "text-xs font-medium", children: "Media" }),
806
- 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: [
807
- /* @__PURE__ */ jsx5("span", { className: "rounded bg-muted px-1 text-[10px] uppercase text-muted-foreground", "data-asset-kind": "", children: asset.kind }),
808
- /* @__PURE__ */ jsx5("span", { className: "min-w-0 flex-1 truncate", title: asset.src, children: asset.name ?? asset.id }),
809
- asset.duration !== void 0 && /* @__PURE__ */ jsx5("span", { className: "tabular-nums text-muted-foreground", children: formatVideoSeconds3(asset.duration) }),
810
- editable && /* @__PURE__ */ jsx5("button", { type: "button", className: btn3, disabled: !canEdit, onClick: () => addToTimeline(asset), "data-add-to-timeline": "", children: "Add to timeline" })
811
- ] }, 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: [
812
880
  "No media yet.",
813
881
  editable ? " Choose or drop files, or add a URL." : ""
814
882
  ] }),
815
- editable && /* @__PURE__ */ jsxs4(Fragment4, { children: [
816
- /* @__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: [
817
885
  "Choose or drop video, audio or image files",
818
- /* @__PURE__ */ jsx5(
886
+ /* @__PURE__ */ jsx6(
819
887
  "input",
820
888
  {
821
889
  type: "file",
@@ -832,7 +900,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMe
832
900
  }
833
901
  )
834
902
  ] }),
835
- /* @__PURE__ */ jsxs4(
903
+ /* @__PURE__ */ jsxs5(
836
904
  "form",
837
905
  {
838
906
  className: "flex items-center gap-1",
@@ -841,7 +909,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMe
841
909
  void addUrl();
842
910
  },
843
911
  children: [
844
- /* @__PURE__ */ jsx5(
912
+ /* @__PURE__ */ jsx6(
845
913
  "input",
846
914
  {
847
915
  className: "min-w-0 flex-1 rounded border border-border bg-transparent px-1 py-0.5 text-xs",
@@ -852,26 +920,26 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = probeVideoMe
852
920
  onChange: (e) => setUrl(e.target.value)
853
921
  }
854
922
  ),
855
- /* @__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: [
856
- /* @__PURE__ */ jsx5("option", { value: "", children: "Auto" }),
857
- /* @__PURE__ */ jsx5("option", { value: "video", children: "Video" }),
858
- /* @__PURE__ */ jsx5("option", { value: "audio", children: "Audio" }),
859
- /* @__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" })
860
928
  ] }),
861
- /* @__PURE__ */ jsx5("button", { type: "submit", className: btn3, 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" })
862
930
  ]
863
931
  }
864
932
  )
865
933
  ] }),
866
- busy > 0 && /* @__PURE__ */ jsx5("p", { className: "text-xs text-muted-foreground", children: "Reading media\u2026" }),
867
- 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 })
868
936
  ]
869
937
  }
870
938
  );
871
939
  }
872
940
 
873
941
  // src/video-editor/TranscriptPanel.tsx
874
- import React4, { useMemo as useMemo3, useState as useState3 } from "react";
942
+ import React5, { useMemo as useMemo3, useState as useState4 } from "react";
875
943
  import {
876
944
  findVideoFillers,
877
945
  findVideoTranscriptPauses,
@@ -881,15 +949,15 @@ import {
881
949
  videoWordAt,
882
950
  videoWordsRange
883
951
  } from "@pixerate/editor/video-editor";
884
- import { Fragment as Fragment5, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
952
+ import { Fragment as Fragment5, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
885
953
  var btn4 = "rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40";
886
954
  function TranscriptPanel({ controller, track, editable = true, className = "" }) {
887
955
  const state = useVideoEditorState(controller);
888
956
  const { project, time, readOnly } = state;
889
957
  const canEdit = editable && !readOnly;
890
- const [anchor, setAnchor] = useState3(null);
891
- const [focus, setFocus] = useState3(null);
892
- const [message, setMessage] = useState3(null);
958
+ const [anchor, setAnchor] = useState4(null);
959
+ const [focus, setFocus] = useState4(null);
960
+ const [message, setMessage] = useState4(null);
893
961
  const words = useMemo3(() => {
894
962
  try {
895
963
  return videoTimelineTranscript(project, { track });
@@ -916,9 +984,9 @@ function TranscriptPanel({ controller, track, editable = true, className = "" })
916
984
  setMessage(e instanceof Error ? e.message : String(e));
917
985
  }
918
986
  };
919
- return /* @__PURE__ */ jsxs5("div", { className: `flex flex-col gap-2 text-xs ${className}`, "data-transcript-panel": "", children: [
920
- words.length > 0 ? /* @__PURE__ */ jsxs5(Fragment5, { children: [
921
- /* @__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(
922
990
  "div",
923
991
  {
924
992
  className: "max-h-48 overflow-y-auto leading-6",
@@ -934,8 +1002,8 @@ function TranscriptPanel({ controller, track, editable = true, className = "" })
934
1002
  },
935
1003
  children: words.map((w, i) => {
936
1004
  const inSelection = selected !== null && i >= selected[0] && i <= selected[1];
937
- return /* @__PURE__ */ jsxs5(React4.Fragment, { children: [
938
- /* @__PURE__ */ jsx6(
1005
+ return /* @__PURE__ */ jsxs6(React5.Fragment, { children: [
1006
+ /* @__PURE__ */ jsx7(
939
1007
  "span",
940
1008
  {
941
1009
  role: "option",
@@ -961,51 +1029,52 @@ function TranscriptPanel({ controller, track, editable = true, className = "" })
961
1029
  })
962
1030
  }
963
1031
  ),
964
- canEdit && /* @__PURE__ */ jsxs5("div", { className: "flex flex-wrap gap-1", children: [
965
- /* @__PURE__ */ jsx6("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" }),
966
- /* @__PURE__ */ jsx6("button", { type: "button", className: btn4, onClick: () => cut(findVideoFillers(words), "Remove fillers", "No filler words found."), children: "Remove fillers" }),
967
- /* @__PURE__ */ jsx6("button", { type: "button", className: btn4, 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" })
968
1036
  ] })
969
- ] }) : /* @__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." }),
970
- canEdit && sound.length > 0 && /* @__PURE__ */ jsxs5("details", { children: [
971
- /* @__PURE__ */ jsx6("summary", { className: "cursor-pointer text-muted-foreground", children: "Transcripts" }),
972
- /* @__PURE__ */ jsx6("ul", { className: "mt-1 space-y-1", children: sound.map((a) => /* @__PURE__ */ jsxs5("li", { className: "flex items-center gap-2", children: [
973
- /* @__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: [
974
1042
  a.name ?? a.id,
975
1043
  " ",
976
1044
  a.transcript ? `\xB7 ${a.transcript.length} words` : ""
977
1045
  ] }),
978
- /* @__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]) })
979
1047
  ] }, a.id)) })
980
1048
  ] }),
981
- 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 })
982
1050
  ] });
983
1051
  }
984
1052
 
985
1053
  // src/video-editor/VideoEditor.tsx
986
- import { useEffect as useEffect4 } from "react";
987
- import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
988
- 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 = "" }) {
989
1057
  const { controller, state } = useVideoEditor(given ?? options ?? {});
990
- useEffect4(() => {
1058
+ useEffect5(() => {
991
1059
  if (editable) return;
992
1060
  const was = controller.getState().readOnly;
993
1061
  controller.setReadOnly(true);
994
1062
  return () => controller.setReadOnly(was);
995
1063
  }, [controller, editable]);
996
1064
  const { issues } = state;
997
- 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)) });
998
- return /* @__PURE__ */ jsxs6("div", { className: `flex flex-col gap-3 ${className}`, "data-video-editor": "", children: [
999
- /* @__PURE__ */ jsxs6("div", { className: `grid gap-3 ${editable ? "lg:grid-cols-[16rem_minmax(0,1fr)_18rem]" : ""}`, children: [
1000
- editable && /* @__PURE__ */ jsx7(MediaBin, { controller, className: "min-w-0" }),
1001
- /* @__PURE__ */ jsxs6("div", { className: "flex min-w-0 flex-col gap-2", children: [
1002
- /* @__PURE__ */ jsx7(VideoPreview, { controller, resolution, className: "rounded-md border border-border" }),
1003
- /* @__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 })
1004
1073
  ] }),
1005
- editable && /* @__PURE__ */ jsx7(ClipInspector, { controller, className: "min-w-0" })
1074
+ editable && /* @__PURE__ */ jsx8(ClipInspector, { controller, className: "min-w-0" })
1006
1075
  ] }),
1007
- /* @__PURE__ */ jsx7(VideoTimeline, { controller, editable, pxPerSecond, onPxPerSecondChange }),
1008
- showTranscript && /* @__PURE__ */ jsx7(TranscriptPanel, { controller, editable }),
1076
+ /* @__PURE__ */ jsx8(VideoTimeline, { controller, editable, pxPerSecond, onPxPerSecondChange }),
1077
+ showTranscript && /* @__PURE__ */ jsx8(TranscriptPanel, { controller, editable }),
1009
1078
  issueList
1010
1079
  ] });
1011
1080
  }
@@ -1015,6 +1084,7 @@ export {
1015
1084
  TranscriptPanel,
1016
1085
  VideoEditor,
1017
1086
  VideoPreview,
1087
+ VideoScopes,
1018
1088
  VideoTimeline,
1019
1089
  VideoTransport,
1020
1090
  useVideoEditor,