@pixerate/editor-react 0.56.0 → 0.58.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.
@@ -35,6 +35,7 @@ __export(video_editor_exports, {
35
35
  TranscriptPanel: () => TranscriptPanel,
36
36
  VideoEditor: () => VideoEditor,
37
37
  VideoPreview: () => VideoPreview,
38
+ VideoScopes: () => VideoScopes,
38
39
  VideoTimeline: () => VideoTimeline,
39
40
  VideoTransport: () => VideoTransport,
40
41
  useVideoEditor: () => useVideoEditor,
@@ -84,11 +85,11 @@ function useVideoEditor(controllerOrOptions = {}) {
84
85
  return { controller, state, ...actions };
85
86
  }
86
87
  function useVideoEditorTools(registry, controller, options = {}) {
87
- const { id, namespace, readOnly, rasterize } = options;
88
+ const { id, namespace, readOnly, rasterize, framePixels, framePixelsTimeout } = options;
88
89
  (0, import_react.useEffect)(() => {
89
90
  if (!registry) return;
90
- return registry.register((0, import_video_editor.createVideoEditorToolProvider)(controller, { id, namespace, readOnly, rasterize }));
91
- }, [registry, controller, id, namespace, readOnly, rasterize]);
91
+ return registry.register((0, import_video_editor.createVideoEditorToolProvider)(controller, { id, namespace, readOnly, rasterize, framePixels, framePixelsTimeout }));
92
+ }, [registry, controller, id, namespace, readOnly, rasterize, framePixels, framePixelsTimeout]);
92
93
  }
93
94
 
94
95
  // src/video-editor/VideoPreview.tsx
@@ -119,28 +120,96 @@ function VideoPreview({ controller, resolution, onFrame, className = "", style }
119
120
  );
120
121
  }
121
122
 
122
- // src/video-editor/VideoTransport.tsx
123
+ // src/video-editor/VideoScopes.tsx
124
+ var import_react3 = require("react");
123
125
  var import_video_editor3 = require("@pixerate/editor/video-editor");
124
126
  var import_jsx_runtime2 = require("react/jsx-runtime");
127
+ var LABELS = { histogram: "Histogram", waveform: "Luma waveform", vectorscope: "Vectorscope" };
128
+ var pct = (v) => `${Math.round(v * 1e3) / 10}%`;
129
+ function VideoScopes({ controller, scopes = import_video_editor3.VIDEO_SCOPE_KINDS, width = 256, height = 128, className = "" }) {
130
+ const canvases = (0, import_react3.useRef)({});
131
+ const [reading, setReading] = (0, import_react3.useState)(() => ({ ok: false, message: "Show the preview to see its scopes." }));
132
+ const key = scopes.join(",");
133
+ (0, import_react3.useEffect)(() => {
134
+ const feed = controller.previewPixels;
135
+ let frame = 0;
136
+ const raf = typeof requestAnimationFrame === "function" ? requestAnimationFrame : (fn) => setTimeout(fn, 16);
137
+ const caf = typeof cancelAnimationFrame === "function" ? cancelAnimationFrame : (id) => clearTimeout(id);
138
+ const draw = () => {
139
+ frame = 0;
140
+ const next = (0, import_video_editor3.readVideoScopesPanel)(feed, scopes, { width, height });
141
+ setReading(next);
142
+ if (!next.ok) return;
143
+ for (const kind of scopes) {
144
+ const img = next.images[kind];
145
+ const ctx = canvases.current[kind]?.getContext("2d");
146
+ if (img && ctx && typeof ImageData !== "undefined") ctx.putImageData(new ImageData(img.data, img.width, img.height), 0, 0);
147
+ }
148
+ };
149
+ const schedule = () => {
150
+ if (!frame) frame = raf(draw);
151
+ };
152
+ schedule();
153
+ const off = feed.subscribe(schedule);
154
+ return () => {
155
+ off();
156
+ if (frame) caf(frame);
157
+ };
158
+ }, [controller, key, width, height]);
159
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `flex flex-col gap-2 text-xs ${className}`, "data-video-scopes": "", children: [
160
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "flex flex-wrap gap-2", children: scopes.map((kind) => {
161
+ const w = kind === "vectorscope" ? height : width;
162
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("figure", { className: "m-0 flex min-w-0 max-w-full flex-col gap-1", children: [
163
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("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 }),
164
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("figcaption", { className: "text-[11px] text-muted-foreground", children: LABELS[kind] })
165
+ ] }, kind);
166
+ }) }),
167
+ reading.ok ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex flex-col gap-0.5", "data-scope-stats": "", children: [
168
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "tabular-nums", children: [
169
+ "Luma ",
170
+ reading.stats.luma.mean.toFixed(2),
171
+ " mean \xB7 ",
172
+ reading.stats.luma.p1.toFixed(2),
173
+ "\u2013",
174
+ reading.stats.luma.p99.toFixed(2),
175
+ " \xB7 crushed ",
176
+ pct(reading.stats.clipped.shadows),
177
+ " \xB7 blown ",
178
+ pct(reading.stats.clipped.highlights),
179
+ " \xB7 average",
180
+ " ",
181
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "inline-block h-2.5 w-2.5 rounded-sm align-middle", style: { background: reading.stats.average } }),
182
+ " ",
183
+ reading.stats.average,
184
+ !reading.complete && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "text-muted-foreground", children: " \xB7 loading" })
185
+ ] }),
186
+ reading.notes.map((n) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "text-amber-500", children: n }, n))
187
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "text-muted-foreground", "data-scope-message": "", children: reading.message })
188
+ ] });
189
+ }
190
+
191
+ // src/video-editor/VideoTransport.tsx
192
+ var import_video_editor4 = require("@pixerate/editor/video-editor");
193
+ var import_jsx_runtime3 = require("react/jsx-runtime");
125
194
  var btn = "rounded border border-border px-2 py-0.5 text-xs disabled:opacity-40";
126
195
  function VideoTransport({ controller, editable = true, className = "" }) {
127
196
  const state = useVideoEditorState(controller);
128
197
  const { time, duration, playing, project, selection, readOnly } = state;
129
198
  const canEdit = editable && !readOnly;
130
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `flex flex-wrap items-center gap-1.5 text-xs ${className}`, "data-video-transport": "", children: [
131
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", className: btn, onClick: () => controller.step(-1), "aria-label": "Previous frame", title: "Previous frame (\u2190)", "data-step-back": "", children: "\u25C0|" }),
132
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("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" }),
133
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", className: btn, onClick: () => controller.step(1), "aria-label": "Next frame", title: "Next frame (\u2192)", "data-step-forward": "", children: "|\u25B6" }),
134
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "px-1 tabular-nums text-muted-foreground", "data-timecode": "", children: [
135
- (0, import_video_editor3.formatVideoTimecode)(time, project.fps),
199
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: `flex flex-wrap items-center gap-1.5 text-xs ${className}`, "data-video-transport": "", children: [
200
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: btn, onClick: () => controller.step(-1), "aria-label": "Previous frame", title: "Previous frame (\u2190)", "data-step-back": "", children: "\u25C0|" }),
201
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("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" }),
202
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: btn, onClick: () => controller.step(1), "aria-label": "Next frame", title: "Next frame (\u2192)", "data-step-forward": "", children: "|\u25B6" }),
203
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "px-1 tabular-nums text-muted-foreground", "data-timecode": "", children: [
204
+ (0, import_video_editor4.formatVideoTimecode)(time, project.fps),
136
205
  " / ",
137
- (0, import_video_editor3.formatVideoTimecode)(duration, project.fps)
206
+ (0, import_video_editor4.formatVideoTimecode)(duration, project.fps)
138
207
  ] }),
139
- editable && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
140
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "mx-1 h-4 w-px bg-border" }),
141
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", className: btn, onClick: () => controller.splitAtPlayhead(), disabled: !canEdit, title: "Split at the playhead (S)", "data-split": "", children: "Split" }),
142
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", className: btn, onClick: () => controller.deleteSelection(), disabled: !canEdit || !selection.length, title: "Delete the selected clips (Delete)", "data-delete": "", children: "Delete" }),
143
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
208
+ editable && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
209
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "mx-1 h-4 w-px bg-border" }),
210
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: btn, onClick: () => controller.splitAtPlayhead(), disabled: !canEdit, title: "Split at the playhead (S)", "data-split": "", children: "Split" }),
211
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: btn, onClick: () => controller.deleteSelection(), disabled: !canEdit || !selection.length, title: "Delete the selected clips (Delete)", "data-delete": "", children: "Delete" }),
212
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
144
213
  "button",
145
214
  {
146
215
  type: "button",
@@ -152,16 +221,16 @@ function VideoTransport({ controller, editable = true, className = "" }) {
152
221
  children: "Ripple delete"
153
222
  }
154
223
  ),
155
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("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" }),
156
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("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" })
224
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("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" }),
225
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("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" })
157
226
  ] })
158
227
  ] });
159
228
  }
160
229
 
161
230
  // src/video-editor/VideoTimeline.tsx
162
- var import_react3 = require("react");
163
- var import_video_editor4 = require("@pixerate/editor/video-editor");
164
- var import_jsx_runtime3 = require("react/jsx-runtime");
231
+ var import_react4 = require("react");
232
+ var import_video_editor5 = require("@pixerate/editor/video-editor");
233
+ var import_jsx_runtime4 = require("react/jsx-runtime");
165
234
  var HEADER = 150;
166
235
  var RULER = 24;
167
236
  var ROW = 44;
@@ -170,48 +239,48 @@ var DRAG_THRESHOLD = 3;
170
239
  var MIN_ZOOM = 10;
171
240
  var MAX_ZOOM = 400;
172
241
  function ClipWaveform({ controller, clip, asset, width, height }) {
173
- const peaks = (0, import_react3.useSyncExternalStore)(
242
+ const peaks = (0, import_react4.useSyncExternalStore)(
174
243
  (notify) => controller.audioPeaks.subscribe(notify),
175
244
  () => controller.audioPeaks.peek(asset),
176
245
  () => void 0
177
246
  );
178
- (0, import_react3.useEffect)(() => {
247
+ (0, import_react4.useEffect)(() => {
179
248
  controller.audioPeaks.load(asset).catch(() => {
180
249
  });
181
250
  }, [controller, asset]);
182
- const d = (0, import_react3.useMemo)(() => peaks ? (0, import_video_editor4.videoClipWaveformPath)(clip, peaks, width, height) : "", [peaks, clip, width, height]);
251
+ const d = (0, import_react4.useMemo)(() => peaks ? (0, import_video_editor5.videoClipWaveformPath)(clip, peaks, width, height) : "", [peaks, clip, width, height]);
183
252
  if (!d) return null;
184
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { className: "pointer-events-none absolute inset-0", width, height, "aria-hidden": "true", "data-waveform": clip.id, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d, fill: "currentColor", fillOpacity: 0.45 }) });
253
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("svg", { className: "pointer-events-none absolute inset-0", width, height, "aria-hidden": "true", "data-waveform": clip.id, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d, fill: "currentColor", fillOpacity: 0.45 }) });
185
254
  }
186
255
  var isField = (el) => el instanceof HTMLElement && /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName);
187
256
  function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable = true, waveforms = "audio", className = "" }) {
188
257
  const state = useVideoEditorState(controller);
189
258
  const { project, duration, time, selection, readOnly, error } = state;
190
- const [ownZoom, setOwnZoom] = (0, import_react3.useState)(pxPerSecond ?? 100);
259
+ const [ownZoom, setOwnZoom] = (0, import_react4.useState)(pxPerSecond ?? 100);
191
260
  const px = onPxPerSecondChange ? pxPerSecond ?? 100 : ownZoom;
192
261
  const canEdit = editable && !readOnly;
193
- const [drag, setDrag] = (0, import_react3.useState)(null);
194
- const dragRef = (0, import_react3.useRef)(null);
195
- const rows = (0, import_react3.useRef)(/* @__PURE__ */ new Map());
196
- const ruler = (0, import_react3.useRef)(null);
197
- const assets = (0, import_video_editor4.videoAssetMap)(project);
262
+ const [drag, setDrag] = (0, import_react4.useState)(null);
263
+ const dragRef = (0, import_react4.useRef)(null);
264
+ const rows = (0, import_react4.useRef)(/* @__PURE__ */ new Map());
265
+ const ruler = (0, import_react4.useRef)(null);
266
+ const assets = (0, import_video_editor5.videoAssetMap)(project);
198
267
  const msToPx = (ms) => ms * px / 1e3;
199
268
  const span = Math.max(duration + 5e3, 1e4);
200
269
  const width = msToPx(span);
201
- const ticks = (0, import_video_editor4.videoRulerTicks)(span, px, project.fps);
270
+ const ticks = (0, import_video_editor5.videoRulerTicks)(span, px, project.fps);
202
271
  const seekAt = (clientX) => {
203
272
  const box = ruler.current.getBoundingClientRect();
204
273
  controller.seek(Math.max(0, (clientX - box.left) * 1e3 / px));
205
274
  };
206
275
  const trackAt = (clipId, clientY) => {
207
- const allowed = (0, import_video_editor4.videoDropTracks)(project, clipId);
276
+ const allowed = (0, import_video_editor5.videoDropTracks)(project, clipId);
208
277
  for (const [id, el] of rows.current) {
209
278
  const box = el.getBoundingClientRect();
210
279
  if (clientY >= box.top && clientY < box.bottom && allowed.includes(id)) return id;
211
280
  }
212
281
  return void 0;
213
282
  };
214
- const planFor = (d, e) => (0, import_video_editor4.planVideoClipDrag)(project, {
283
+ const planFor = (d, e) => (0, import_video_editor5.planVideoClipDrag)(project, {
215
284
  clip: d.clip,
216
285
  mode: d.mode,
217
286
  delta: (e.clientX - d.x) * 1e3 / px,
@@ -224,10 +293,10 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
224
293
  dragRef.current = next;
225
294
  setDrag(next);
226
295
  };
227
- const live = (0, import_react3.useRef)({ planFor, selection });
296
+ const live = (0, import_react4.useRef)({ planFor, selection });
228
297
  live.current = { planFor, selection };
229
- const stopDrag = (0, import_react3.useRef)(null);
230
- (0, import_react3.useEffect)(() => () => stopDrag.current?.(), []);
298
+ const stopDrag = (0, import_react4.useRef)(null);
299
+ (0, import_react4.useEffect)(() => () => stopDrag.current?.(), []);
231
300
  const onClipPointerDown = (e, clip, track) => {
232
301
  if (e.button !== void 0 && e.button !== 0) return;
233
302
  e.stopPropagation();
@@ -259,7 +328,7 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
259
328
  return;
260
329
  }
261
330
  const plan = live.current.planFor(d, ev);
262
- if (plan?.edit) controller.apply(plan.edit, (0, import_video_editor4.videoDragLabel)(plan.edit));
331
+ if (plan?.edit) controller.apply(plan.edit, (0, import_video_editor5.videoDragLabel)(plan.edit));
263
332
  };
264
333
  const cancel = () => {
265
334
  stop();
@@ -305,7 +374,7 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
305
374
  }).filter((x) => x !== null)
306
375
  )
307
376
  }));
308
- const toggle = (track, field2, label) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
377
+ const toggle = (track, field2, label) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
309
378
  "button",
310
379
  {
311
380
  type: "button",
@@ -319,12 +388,12 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
319
388
  children: field2 === "muted" ? "M" : field2 === "hidden" ? "H" : "L"
320
389
  }
321
390
  );
322
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: `flex flex-col gap-1 ${className}`, "data-video-timeline-wrapper": "", children: [
323
- !onPxPerSecondChange && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "flex items-center gap-2 self-end text-xs text-muted-foreground", children: [
391
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: `flex flex-col gap-1 ${className}`, "data-video-timeline-wrapper": "", children: [
392
+ !onPxPerSecondChange && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "flex items-center gap-2 self-end text-xs text-muted-foreground", children: [
324
393
  "Zoom",
325
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("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": "" })
394
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("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": "" })
326
395
  ] }),
327
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
396
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
328
397
  "div",
329
398
  {
330
399
  className: "relative overflow-x-auto rounded border border-border outline-none focus-visible:ring-2 focus-visible:ring-rose-500/50",
@@ -333,10 +402,10 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
333
402
  "aria-label": "Video timeline",
334
403
  onKeyDown,
335
404
  "data-video-timeline": "",
336
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "relative", style: { width: HEADER + width }, children: [
337
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex", style: { height: RULER }, children: [
338
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "sticky left-0 z-20 shrink-0 border-b border-r border-border bg-background", style: { width: HEADER } }),
339
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
405
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "relative", style: { width: HEADER + width }, children: [
406
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex", style: { height: RULER }, children: [
407
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "sticky left-0 z-20 shrink-0 border-b border-r border-border bg-background", style: { width: HEADER } }),
408
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
340
409
  "div",
341
410
  {
342
411
  ref: ruler,
@@ -359,13 +428,13 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
359
428
  if (e.buttons & 1) seekAt(e.clientX);
360
429
  },
361
430
  children: [
362
- ticks.map((t) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("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__ */ (0, import_jsx_runtime3.jsx)("span", { className: "absolute -top-3.5 left-1 whitespace-nowrap tabular-nums", children: t.label }) }, t.time)),
363
- (project.markers ?? []).map((m) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
431
+ ticks.map((t) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("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__ */ (0, import_jsx_runtime4.jsx)("span", { className: "absolute -top-3.5 left-1 whitespace-nowrap tabular-nums", children: t.label }) }, t.time)),
432
+ (project.markers ?? []).map((m) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
364
433
  "div",
365
434
  {
366
435
  className: "absolute top-0 h-2.5 w-2.5 -translate-x-1/2 rotate-45",
367
436
  style: { left: msToPx(m.time), background: m.color ?? "#f59e0b" },
368
- title: `${m.label ?? m.id} at ${(0, import_video_editor4.formatVideoSeconds)(m.time)}`,
437
+ title: `${m.label ?? m.id} at ${(0, import_video_editor5.formatVideoSeconds)(m.time)}`,
369
438
  "data-marker": m.id
370
439
  },
371
440
  m.id
@@ -374,19 +443,19 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
374
443
  }
375
444
  )
376
445
  ] }),
377
- placed.map(({ track, clips }) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex border-b border-border last:border-b-0", style: { height: ROW }, "data-track": track.id, children: [
378
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("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: [
379
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center gap-1", children: [
380
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "truncate font-medium", children: track.name ?? track.id }),
381
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "rounded bg-muted px-1 text-[10px] uppercase text-muted-foreground", "data-track-kind": "", children: track.kind })
446
+ placed.map(({ track, clips }) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex border-b border-border last:border-b-0", style: { height: ROW }, "data-track": track.id, children: [
447
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("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: [
448
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex items-center gap-1", children: [
449
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "truncate font-medium", children: track.name ?? track.id }),
450
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "rounded bg-muted px-1 text-[10px] uppercase text-muted-foreground", "data-track-kind": "", children: track.kind })
382
451
  ] }),
383
- editable && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex gap-1", children: [
452
+ editable && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex gap-1", children: [
384
453
  toggle(track, "muted", "Mute"),
385
454
  track.kind === "video" && toggle(track, "hidden", "Hide"),
386
455
  toggle(track, "locked", "Lock")
387
456
  ] })
388
457
  ] }),
389
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
458
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
390
459
  "div",
391
460
  {
392
461
  ref: (el) => {
@@ -402,35 +471,35 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
402
471
  children: clips.map(({ clip, home, start, duration: length, dragging }) => {
403
472
  const asset = clip.type === "media" ? assets.get(clip.asset) : void 0;
404
473
  const selected = selection.includes(clip.id);
405
- const label = (0, import_video_editor4.videoClipLabel)(clip, asset);
406
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
474
+ const label = (0, import_video_editor5.videoClipLabel)(clip, asset);
475
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
407
476
  "div",
408
477
  {
409
478
  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" : ""}`,
410
479
  style: {
411
480
  left: msToPx(start),
412
481
  width: Math.max(2, msToPx(length)),
413
- background: (0, import_video_editor4.videoClipTint)(clip, asset),
482
+ background: (0, import_video_editor5.videoClipTint)(clip, asset),
414
483
  outline: selected ? "2px solid #f43f5e" : void 0,
415
484
  outlineOffset: selected ? 1 : void 0,
416
485
  zIndex: dragging ? 10 : selected ? 2 : 1
417
486
  },
418
- title: `${label} \xB7 ${(0, import_video_editor4.formatVideoSeconds)(start)}\u2013${(0, import_video_editor4.formatVideoSeconds)(start + length)}`,
487
+ title: `${label} \xB7 ${(0, import_video_editor5.formatVideoSeconds)(start)}\u2013${(0, import_video_editor5.formatVideoSeconds)(start + length)}`,
419
488
  "data-clip": clip.id,
420
489
  "data-selected": selected ? "" : void 0,
421
490
  "data-dragging": dragging ? "" : void 0,
422
491
  "aria-selected": selected,
423
492
  onPointerDown: (e) => onClipPointerDown(e, clip, home),
424
493
  children: [
425
- (0, import_video_editor4.videoClipShowsWaveform)(clip, asset, waveforms) && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ClipWaveform, { controller, clip, asset, width: Math.max(2, msToPx(clip.duration)), height: ROW - 8 }),
426
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "pointer-events-none relative block truncate px-1.5 py-0.5 leading-4", children: label }),
427
- clip.transition && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("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: [
494
+ (0, import_video_editor5.videoClipShowsWaveform)(clip, asset, waveforms) && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ClipWaveform, { controller, clip, asset, width: Math.max(2, msToPx(clip.duration)), height: ROW - 8 }),
495
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "pointer-events-none relative block truncate px-1.5 py-0.5 leading-4", children: label }),
496
+ clip.transition && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("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: [
428
497
  "\u29D3 ",
429
498
  clip.transition.type
430
499
  ] }),
431
- canEdit && !home.locked && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
432
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("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" }),
433
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("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" })
500
+ canEdit && !home.locked && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
501
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("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" }),
502
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("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" })
434
503
  ] })
435
504
  ]
436
505
  },
@@ -440,17 +509,18 @@ function VideoTimeline({ controller, pxPerSecond, onPxPerSecondChange, editable
440
509
  }
441
510
  )
442
511
  ] }, track.id)),
443
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("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": "" })
512
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("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": "" })
444
513
  ] })
445
514
  }
446
515
  ),
447
- error && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-xs text-red-500", role: "alert", "data-edit-error": "", children: error })
516
+ error && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "text-xs text-red-500", role: "alert", "data-edit-error": "", children: error })
448
517
  ] });
449
518
  }
450
519
 
451
520
  // src/video-editor/ClipInspector.tsx
452
- var import_video_editor5 = require("@pixerate/editor/video-editor");
453
- var import_jsx_runtime4 = require("react/jsx-runtime");
521
+ var import_react5 = require("react");
522
+ var import_video_editor6 = require("@pixerate/editor/video-editor");
523
+ var import_jsx_runtime5 = require("react/jsx-runtime");
454
524
  var btn2 = "rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40";
455
525
  var field = "w-full rounded border border-border bg-transparent px-1 py-0.5 text-xs disabled:opacity-50";
456
526
  var DEFAULT_TRANSITION = 500;
@@ -464,7 +534,7 @@ function Commit({
464
534
  if (raw !== value) onCommit(raw);
465
535
  };
466
536
  if (multiline)
467
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
537
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
468
538
  "textarea",
469
539
  {
470
540
  className: field,
@@ -476,11 +546,64 @@ function Commit({
476
546
  },
477
547
  value
478
548
  );
479
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("input", { className: field, ...rest, defaultValue: value, onKeyDown: (e) => e.key === "Enter" && e.currentTarget.blur(), onBlur: (e) => commit(e.currentTarget.value) }, value);
549
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("input", { className: field, ...rest, defaultValue: value, onKeyDown: (e) => e.key === "Enter" && e.currentTarget.blur(), onBlur: (e) => commit(e.currentTarget.value) }, value);
550
+ }
551
+ function LutControls({ controller, clip, disabled }) {
552
+ const [message, setMessage] = (0, import_react5.useState)(null);
553
+ const lut = clip.grade?.lut;
554
+ const apply = (setting, label) => {
555
+ const r = controller.apply((0, import_video_editor6.videoLutEdits)(controller.getProject(), [clip.id], setting), label);
556
+ setMessage(r.ok ? null : r.error);
557
+ };
558
+ const choose = async (src) => {
559
+ if (!src) return lut && apply(null, "Remove LUT");
560
+ try {
561
+ const table = await controller.luts.load(src);
562
+ apply({ src, ...lut?.strength !== void 0 ? { strength: lut.strength } : {}, ...table.title ? { name: table.title } : {} }, "Apply LUT");
563
+ } catch (e) {
564
+ setMessage(e instanceof Error ? e.message : String(e));
565
+ }
566
+ };
567
+ const readFile = async (file) => {
568
+ if (!file) return;
569
+ try {
570
+ const table = controller.luts.set(file.name, await file.text());
571
+ apply({ src: file.name, ...lut?.strength !== void 0 ? { strength: lut.strength } : {}, ...table.title ? { name: table.title } : {} }, "Apply LUT");
572
+ } catch (e) {
573
+ setMessage(`${file.name}: ${e instanceof Error ? e.message : String(e)}`);
574
+ }
575
+ };
576
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
577
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "LUT", name: "grade.lut", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex min-w-0 gap-1", children: [
578
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Commit, { value: lut?.src ?? "", placeholder: "look.cube", "aria-label": "LUT file", disabled, onCommit: (raw) => void choose(raw.trim()) }),
579
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { className: `${btn2} cursor-pointer whitespace-nowrap`, title: "Load a .cube file", children: [
580
+ "Load",
581
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("input", { type: "file", accept: ".cube", className: "sr-only", disabled, "aria-label": "Load a .cube LUT", onChange: (e) => void readFile(e.currentTarget.files?.[0]) })
582
+ ] }),
583
+ lut && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: btn2, disabled, "aria-label": "Remove LUT", onClick: () => apply(null, "Remove LUT"), children: "\u2715" })
584
+ ] }) }),
585
+ lut && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "LUT strength", name: "grade.lut.strength", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
586
+ Commit,
587
+ {
588
+ type: "number",
589
+ min: 0,
590
+ max: 1,
591
+ step: 0.05,
592
+ value: String(lut.strength ?? 1),
593
+ "aria-label": "LUT strength",
594
+ disabled,
595
+ onCommit: (raw) => {
596
+ const n = Number(raw);
597
+ if (raw.trim() && Number.isFinite(n)) apply(n > 0 ? { ...lut, strength: Math.min(1, n) } : null, n > 0 ? "Change LUT strength" : "Remove LUT");
598
+ }
599
+ }
600
+ ) }),
601
+ message && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-xs text-red-500", role: "alert", "data-lut-error": "", children: message })
602
+ ] });
480
603
  }
481
604
  function Row({ label, name, children }) {
482
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "grid grid-cols-[5.5rem_minmax(0,1fr)] items-center gap-2 text-xs", "data-field": name, children: [
483
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-muted-foreground", children: label }),
605
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { className: "grid grid-cols-[5.5rem_minmax(0,1fr)] items-center gap-2 text-xs", "data-field": name, children: [
606
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-muted-foreground", children: label }),
484
607
  children
485
608
  ] });
486
609
  }
@@ -488,8 +611,8 @@ function ClipInspector({ controller, className = "" }) {
488
611
  const state = useVideoEditorState(controller);
489
612
  const { project, selection, readOnly, time } = state;
490
613
  if (selection.length !== 1)
491
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("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." });
492
- const at = (0, import_video_editor5.findVideoClip)(project, selection[0]);
614
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("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." });
615
+ const at = (0, import_video_editor6.findVideoClip)(project, selection[0]);
493
616
  if (!at) return null;
494
617
  const clip = at.clip;
495
618
  const disabled = readOnly || !!at.track.locked;
@@ -505,24 +628,24 @@ function ClipInspector({ controller, className = "" }) {
505
628
  };
506
629
  const text = (key, label) => {
507
630
  const value = clip[key];
508
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Commit, { value: value === void 0 ? "" : String(value), "aria-label": label ?? key, disabled, onCommit: (raw) => set(key, raw.trim() || void 0, `Change ${label ?? key}`) });
631
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Commit, { value: value === void 0 ? "" : String(value), "aria-label": label ?? key, disabled, onCommit: (raw) => set(key, raw.trim() || void 0, `Change ${label ?? key}`) });
509
632
  };
510
633
  const number = (key, label, attrs = {}) => {
511
634
  const value = clip[key];
512
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Commit, { type: "number", value: value === void 0 ? "" : String(value), "aria-label": label, disabled, onCommit: (raw) => num(key, raw, `Change ${label.toLowerCase()}`), ...attrs });
635
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Commit, { type: "number", value: value === void 0 ? "" : String(value), "aria-label": label, disabled, onCommit: (raw) => num(key, raw, `Change ${label.toLowerCase()}`), ...attrs });
513
636
  };
514
637
  const transition = clip.transition;
515
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: `flex flex-col gap-1.5 ${className}`, "data-clip-inspector": "", "data-clip-id": clip.id, children: [
516
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex items-center gap-2 text-xs", children: [
517
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "font-medium", children: clip.type === "media" ? "Media clip" : clip.type === "text" ? "Title" : "Colour clip" }),
518
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("code", { className: "text-muted-foreground", children: clip.id }),
519
- at.track.locked && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-amber-500", children: "locked track" })
638
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: `flex flex-col gap-1.5 ${className}`, "data-clip-inspector": "", "data-clip-id": clip.id, children: [
639
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center gap-2 text-xs", children: [
640
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "font-medium", children: clip.type === "media" ? "Media clip" : clip.type === "text" ? "Title" : "Colour clip" }),
641
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("code", { className: "text-muted-foreground", children: clip.id }),
642
+ at.track.locked && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-amber-500", children: "locked track" })
520
643
  ] }),
521
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Name", name: "name", children: text("name", "Name") }),
522
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Start", name: "start", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "tabular-nums text-xs", children: (0, import_video_editor5.formatVideoSeconds)(clip.start) }) }),
523
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Duration", name: "duration", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "tabular-nums text-xs", children: (0, import_video_editor5.formatVideoSeconds)(clip.duration) }) }),
524
- (0, import_video_editor5.videoKeyframeRows)(project, clip.id, time).map((row) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: row.label, name: row.property, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "flex min-w-0 items-center gap-1", "data-keyframe-row": row.property, children: [
525
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
644
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Name", name: "name", children: text("name", "Name") }),
645
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Start", name: "start", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "tabular-nums text-xs", children: (0, import_video_editor6.formatVideoSeconds)(clip.start) }) }),
646
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Duration", name: "duration", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "tabular-nums text-xs", children: (0, import_video_editor6.formatVideoSeconds)(clip.duration) }) }),
647
+ (0, import_video_editor6.videoKeyframeRows)(project, clip.id, time).map((row) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: row.label, name: row.property, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "flex min-w-0 items-center gap-1", "data-keyframe-row": row.property, children: [
648
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
526
649
  Commit,
527
650
  {
528
651
  type: "number",
@@ -536,12 +659,12 @@ function ClipInspector({ controller, className = "" }) {
536
659
  onCommit: (raw) => {
537
660
  const n = Number(raw);
538
661
  if (raw.trim() && !Number.isFinite(n)) return;
539
- const edit = (0, import_video_editor5.videoKeyframeValueEdit)(project, clip.id, row.property, time, raw.trim() ? n : void 0);
662
+ const edit = (0, import_video_editor6.videoKeyframeValueEdit)(project, clip.id, row.property, time, raw.trim() ? n : void 0);
540
663
  if (edit) controller.apply(edit, row.keyed ? `Keyframe ${row.label.toLowerCase()}` : `Change ${row.label.toLowerCase()}`);
541
664
  }
542
665
  }
543
666
  ),
544
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
667
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
545
668
  "button",
546
669
  {
547
670
  type: "button",
@@ -552,17 +675,17 @@ function ClipInspector({ controller, className = "" }) {
552
675
  disabled,
553
676
  "data-keyframe-toggle": row.property,
554
677
  onClick: () => {
555
- const edit = (0, import_video_editor5.videoKeyframeToggleEdit)(project, clip.id, row.property, time);
678
+ const edit = (0, import_video_editor6.videoKeyframeToggleEdit)(project, clip.id, row.property, time);
556
679
  if (edit) controller.apply(edit, `${row.onKey ? "Remove" : "Add"} ${row.label.toLowerCase()} keyframe`);
557
680
  },
558
681
  children: row.onKey ? "\u25C6" : "\u25C7"
559
682
  }
560
683
  ),
561
- row.keyed && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "shrink-0 text-[10px] tabular-nums text-muted-foreground", "data-keyframe-count": row.property, children: row.keys })
684
+ row.keyed && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "shrink-0 text-[10px] tabular-nums text-muted-foreground", "data-keyframe-count": row.property, children: row.keys })
562
685
  ] }) }, row.property)),
563
- at.track.kind === "video" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Blend", name: "blend", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("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: import_video_editor5.VIDEO_BLEND_MODES.map((m) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: m, children: import_video_editor5.VIDEO_BLEND_MODE_LABELS[m] }, m)) }) }),
564
- at.track.kind === "video" && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
565
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Look", name: "gradePreset", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
686
+ at.track.kind === "video" && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Blend", name: "blend", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("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: import_video_editor6.VIDEO_BLEND_MODES.map((m) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: m, children: import_video_editor6.VIDEO_BLEND_MODE_LABELS[m] }, m)) }) }),
687
+ at.track.kind === "video" && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
688
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Look", name: "gradePreset", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
566
689
  "select",
567
690
  {
568
691
  className: field,
@@ -570,16 +693,16 @@ function ClipInspector({ controller, className = "" }) {
570
693
  disabled,
571
694
  "aria-label": "Apply a look",
572
695
  onChange: (e) => {
573
- const grade = (0, import_video_editor5.videoGradePreset)(e.target.value);
696
+ const grade = (0, import_video_editor6.videoGradeLook)(clip.grade, e.target.value);
574
697
  if (grade) controller.apply({ op: "update_clip", clip: clip.id, set: { grade } }, `Apply ${e.target.value} look`);
575
698
  },
576
699
  children: [
577
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: "", children: clip.grade ? "Graded" : "None" }),
578
- import_video_editor5.VIDEO_GRADE_PRESET_NAMES.map((name) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: name, children: name.replace(/-/g, " ") }, name))
700
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "", children: clip.grade ? "Graded" : "None" }),
701
+ import_video_editor6.VIDEO_GRADE_PRESET_NAMES.map((name) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: name, children: name.replace(/-/g, " ") }, name))
579
702
  ]
580
703
  }
581
704
  ) }),
582
- import_video_editor5.VIDEO_GRADE_CONTROLS.map((c) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: c.label, name: `grade.${c.key}`, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
705
+ import_video_editor6.VIDEO_GRADE_CONTROLS.map((c) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: c.label, name: `grade.${c.key}`, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
583
706
  Commit,
584
707
  {
585
708
  type: "number",
@@ -593,13 +716,14 @@ function ClipInspector({ controller, className = "" }) {
593
716
  onCommit: (raw) => {
594
717
  const n = raw.trim() ? Number(raw) : 0;
595
718
  if (!Number.isFinite(n)) return;
596
- const grade = (0, import_video_editor5.normalizeVideoGrade)({ ...clip.grade, [c.key]: n });
719
+ const grade = (0, import_video_editor6.normalizeVideoGrade)({ ...clip.grade, [c.key]: n });
597
720
  set("grade", grade, `Change ${c.label.toLowerCase()}`);
598
721
  }
599
722
  }
600
- ) }, c.key))
723
+ ) }, c.key)),
724
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(LutControls, { controller, clip, disabled })
601
725
  ] }),
602
- at.track.kind === "video" && ["animateIn", "animateOut"].map((key) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: key === "animateIn" ? "Animate in" : "Animate out", name: key, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
726
+ at.track.kind === "video" && ["animateIn", "animateOut"].map((key) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: key === "animateIn" ? "Animate in" : "Animate out", name: key, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
603
727
  "select",
604
728
  {
605
729
  className: field,
@@ -611,35 +735,35 @@ function ClipInspector({ controller, className = "" }) {
611
735
  set(key, effect ? { ...clip[key], effect } : void 0, key === "animateIn" ? "Change entrance" : "Change exit");
612
736
  },
613
737
  children: [
614
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: "", children: "None" }),
615
- import_video_editor5.VIDEO_CLIP_ANIMATION_EFFECTS.map((effect) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: effect, children: import_video_editor5.VIDEO_CLIP_ANIMATION_LABELS[effect] }, effect))
738
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "", children: "None" }),
739
+ import_video_editor6.VIDEO_CLIP_ANIMATION_EFFECTS.map((effect) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: effect, children: import_video_editor6.VIDEO_CLIP_ANIMATION_LABELS[effect] }, effect))
616
740
  ]
617
741
  }
618
742
  ) }, key)),
619
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Fade in (ms)", name: "fadeIn", children: number("fadeIn", "Fade in", { min: 0, step: 100 }) }),
620
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Fade out (ms)", name: "fadeOut", children: number("fadeOut", "Fade out", { min: 0, step: 100 }) }),
621
- clip.type === "media" && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
622
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Speed", name: "speed", children: clip.timeRemap ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "flex items-center gap-1", "data-ramped": "", children: [
623
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-muted-foreground", children: "ramped" }),
624
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("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" })
743
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Fade in (ms)", name: "fadeIn", children: number("fadeIn", "Fade in", { min: 0, step: 100 }) }),
744
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Fade out (ms)", name: "fadeOut", children: number("fadeOut", "Fade out", { min: 0, step: 100 }) }),
745
+ clip.type === "media" && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
746
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Speed", name: "speed", children: clip.timeRemap ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "flex items-center gap-1", "data-ramped": "", children: [
747
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-muted-foreground", children: "ramped" }),
748
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("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" })
625
749
  ] }) : number("speed", "Speed", { min: 0.1, step: 0.25 }) }),
626
- project.assets.find((a) => a.id === clip.asset)?.kind !== "image" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Retime", name: "retime", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "flex flex-wrap gap-1", children: [
627
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
750
+ project.assets.find((a) => a.id === clip.asset)?.kind !== "image" && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Retime", name: "retime", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "flex flex-wrap gap-1", children: [
751
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
628
752
  "button",
629
753
  {
630
754
  type: "button",
631
755
  className: btn2,
632
756
  disabled: disabled || time < clip.start || time > clip.start + clip.duration,
633
757
  title: "Hold the frame at the playhead for a second, pushing later clips along",
634
- onClick: () => controller.apply((0, import_video_editor5.videoFreezeFrameEdit)(clip, time - clip.start, 1e3), "Freeze frame"),
758
+ onClick: () => controller.apply((0, import_video_editor6.videoFreezeFrameEdit)(clip, time - clip.start, 1e3), "Freeze frame"),
635
759
  children: "Freeze 1 s here"
636
760
  }
637
761
  ),
638
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "button", className: btn2, disabled, title: "Normal speed, then a glide into 0.3\xD7 slow motion", onClick: () => controller.apply((0, import_video_editor5.videoSpeedRampEdit)(clip, (0, import_video_editor5.videoSlowMotionRamp)(clip.duration)), "Speed ramp"), children: "Slow-mo ramp" })
762
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: btn2, disabled, title: "Normal speed, then a glide into 0.3\xD7 slow motion", onClick: () => controller.apply((0, import_video_editor6.videoSpeedRampEdit)(clip, (0, import_video_editor6.videoSlowMotionRamp)(clip.duration)), "Speed ramp"), children: "Slow-mo ramp" })
639
763
  ] }) }),
640
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Muted", name: "muted", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("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") }) }),
764
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Muted", name: "muted", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("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") }) }),
641
765
  ["fadeInCurve", "fadeOutCurve"].map(
642
- (key) => (key === "fadeInCurve" ? clip.fadeIn : clip.fadeOut) ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: key === "fadeInCurve" ? "Fade in sound" : "Fade out sound", name: key, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
766
+ (key) => (key === "fadeInCurve" ? clip.fadeIn : clip.fadeOut) ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: key === "fadeInCurve" ? "Fade in sound" : "Fade out sound", name: key, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
643
767
  "select",
644
768
  {
645
769
  className: field,
@@ -650,19 +774,19 @@ function ClipInspector({ controller, className = "" }) {
650
774
  const curve = e.target.value;
651
775
  set(key, curve === "linear" ? void 0 : curve, key === "fadeInCurve" ? "Change fade in curve" : "Change fade out curve");
652
776
  },
653
- children: import_video_editor5.VIDEO_FADE_CURVES.map((c) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: c, children: import_video_editor5.VIDEO_FADE_CURVE_LABELS[c] }, c))
777
+ children: import_video_editor6.VIDEO_FADE_CURVES.map((c) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: c, children: import_video_editor6.VIDEO_FADE_CURVE_LABELS[c] }, c))
654
778
  }
655
779
  ) }, key) : null
656
780
  )
657
781
  ] }),
658
- clip.type === "text" && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
659
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Text", name: "text", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Commit, { multiline: true, value: clip.text, "aria-label": "Text", disabled, onCommit: (raw) => set("text", raw, "Change text") }) }),
660
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Colour", name: "color", children: text("color", "Colour") }),
661
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Font size", name: "fontSize", children: number("fontSize", "Font size", { min: 1, step: 2 }) }),
662
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Background", name: "background", children: text("background", "Background") })
782
+ clip.type === "text" && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
783
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Text", name: "text", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Commit, { multiline: true, value: clip.text, "aria-label": "Text", disabled, onCommit: (raw) => set("text", raw, "Change text") }) }),
784
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Colour", name: "color", children: text("color", "Colour") }),
785
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Font size", name: "fontSize", children: number("fontSize", "Font size", { min: 1, step: 2 }) }),
786
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Background", name: "background", children: text("background", "Background") })
663
787
  ] }),
664
- clip.type === "color" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Colour", name: "color", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Commit, { value: clip.color, "aria-label": "Colour", disabled, onCommit: (raw) => raw.trim() && set("color", raw.trim(), "Change colour") }) }),
665
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Transition in", name: "transition", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
788
+ clip.type === "color" && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Colour", name: "color", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Commit, { value: clip.color, "aria-label": "Colour", disabled, onCommit: (raw) => raw.trim() && set("color", raw.trim(), "Change colour") }) }),
789
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Transition in", name: "transition", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
666
790
  "select",
667
791
  {
668
792
  className: field,
@@ -677,12 +801,12 @@ function ClipInspector({ controller, className = "" }) {
677
801
  );
678
802
  },
679
803
  children: [
680
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: "", children: "None" }),
681
- import_video_editor5.VIDEO_TRANSITION_TYPES.map((t) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: t, children: import_video_editor5.VIDEO_TRANSITION_LABELS[t] }, t))
804
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "", children: "None" }),
805
+ import_video_editor6.VIDEO_TRANSITION_TYPES.map((t) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: t, children: import_video_editor6.VIDEO_TRANSITION_LABELS[t] }, t))
682
806
  ]
683
807
  }
684
808
  ) }),
685
- transition && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Length (ms)", name: "transitionDuration", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
809
+ transition && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Length (ms)", name: "transitionDuration", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
686
810
  Commit,
687
811
  {
688
812
  type: "number",
@@ -697,7 +821,7 @@ function ClipInspector({ controller, className = "" }) {
697
821
  }
698
822
  }
699
823
  ) }),
700
- transition && (0, import_video_editor5.videoTransitionHasDirection)(transition.type) && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Direction", name: "transitionDirection", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
824
+ transition && (0, import_video_editor6.videoTransitionHasDirection)(transition.type) && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Direction", name: "transitionDirection", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
701
825
  "select",
702
826
  {
703
827
  className: field,
@@ -705,41 +829,41 @@ function ClipInspector({ controller, className = "" }) {
705
829
  disabled,
706
830
  "aria-label": "Transition direction",
707
831
  onChange: (e) => controller.apply({ op: "set_transition", clip: clip.id, transition: { ...transition, direction: e.target.value } }, "Change transition direction"),
708
- children: import_video_editor5.VIDEO_WIPE_DIRECTIONS.map((d) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: d, children: d }, d))
832
+ children: import_video_editor6.VIDEO_WIPE_DIRECTIONS.map((d) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: d, children: d }, d))
709
833
  }
710
834
  ) }),
711
- transition && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Transition sound", name: "transitionAudioCurve", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
835
+ transition && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Transition sound", name: "transitionAudioCurve", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
712
836
  "select",
713
837
  {
714
838
  className: field,
715
- value: (0, import_video_editor5.videoTransitionAudioCurve)(transition),
839
+ value: (0, import_video_editor6.videoTransitionAudioCurve)(transition),
716
840
  disabled,
717
841
  "aria-label": "Transition sound curve",
718
842
  onChange: (e) => {
719
843
  const { audioCurve: _, ...rest } = transition;
720
844
  const curve = e.target.value;
721
- const next = curve === (0, import_video_editor5.videoTransitionAudioCurve)(rest) ? rest : { ...rest, audioCurve: curve };
845
+ const next = curve === (0, import_video_editor6.videoTransitionAudioCurve)(rest) ? rest : { ...rest, audioCurve: curve };
722
846
  controller.apply({ op: "set_transition", clip: clip.id, transition: next }, "Change transition sound");
723
847
  },
724
- children: import_video_editor5.VIDEO_FADE_CURVES.map((c) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: c, children: import_video_editor5.VIDEO_FADE_CURVE_LABELS[c] }, c))
848
+ children: import_video_editor6.VIDEO_FADE_CURVES.map((c) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: c, children: import_video_editor6.VIDEO_FADE_CURVE_LABELS[c] }, c))
725
849
  }
726
850
  ) })
727
851
  ] });
728
852
  }
729
853
 
730
854
  // src/video-editor/MediaBin.tsx
731
- var import_react4 = require("react");
732
- var import_video_editor6 = require("@pixerate/editor/video-editor");
733
- var import_jsx_runtime5 = require("react/jsx-runtime");
855
+ var import_react6 = require("react");
856
+ var import_video_editor7 = require("@pixerate/editor/video-editor");
857
+ var import_jsx_runtime6 = require("react/jsx-runtime");
734
858
  var btn3 = "rounded border border-border px-2 py-0.5 text-xs disabled:opacity-40";
735
- function MediaBin({ controller, editable = true, fileToUrl, probe = import_video_editor6.probeVideoMedia, className = "" }) {
859
+ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video_editor7.probeVideoMedia, className = "" }) {
736
860
  const { project, readOnly } = useVideoEditorState(controller);
737
861
  const canEdit = editable && !readOnly;
738
- const [url, setUrl] = (0, import_react4.useState)("");
739
- const [urlKind, setUrlKind] = (0, import_react4.useState)("");
740
- const [busy, setBusy] = (0, import_react4.useState)(0);
741
- const [problem, setProblem] = (0, import_react4.useState)(null);
742
- const [over, setOver] = (0, import_react4.useState)(false);
862
+ const [url, setUrl] = (0, import_react6.useState)("");
863
+ const [urlKind, setUrlKind] = (0, import_react6.useState)("");
864
+ const [busy, setBusy] = (0, import_react6.useState)(0);
865
+ const [problem, setProblem] = (0, import_react6.useState)(null);
866
+ const [over, setOver] = (0, import_react6.useState)(false);
743
867
  const addSource = async (src, kind, name) => {
744
868
  setBusy((n) => n + 1);
745
869
  try {
@@ -753,7 +877,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
753
877
  const addFiles = async (files) => {
754
878
  setProblem(null);
755
879
  for (const file of Array.from(files ?? [])) {
756
- const kind = (0, import_video_editor6.videoAssetKindOf)(file.type || file.name);
880
+ const kind = (0, import_video_editor7.videoAssetKindOf)(file.type || file.name);
757
881
  if (!kind) {
758
882
  setProblem(`"${file.name}" is not a video, audio or image file.`);
759
883
  continue;
@@ -765,7 +889,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
765
889
  const addUrl = async () => {
766
890
  const src = url.trim();
767
891
  if (!src) return;
768
- const kind = urlKind || (0, import_video_editor6.videoAssetKindOf)(src.replace(/[?#].*$/, ""));
892
+ const kind = urlKind || (0, import_video_editor7.videoAssetKindOf)(src.replace(/[?#].*$/, ""));
769
893
  if (!kind) {
770
894
  setProblem(`Can't tell whether "${src}" is video, audio or an image; choose its kind.`);
771
895
  return;
@@ -775,7 +899,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
775
899
  setUrl("");
776
900
  };
777
901
  const addToTimeline = (asset) => {
778
- const edit = (0, import_video_editor6.videoAssetDropEdit)(project, asset.id);
902
+ const edit = (0, import_video_editor7.videoAssetDropEdit)(project, asset.id);
779
903
  if (!edit) {
780
904
  setProblem(`There is no unlocked ${asset.kind === "audio" ? "audio" : "video"} track to put "${asset.name ?? asset.id}" on.`);
781
905
  return;
@@ -787,7 +911,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
787
911
  if (added) controller.select([added]);
788
912
  }
789
913
  };
790
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
914
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
791
915
  "div",
792
916
  {
793
917
  className: `flex flex-col gap-2 rounded ${over ? "ring-2 ring-rose-500/60" : ""} ${className}`,
@@ -805,20 +929,20 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
805
929
  void addFiles(e.dataTransfer.files);
806
930
  },
807
931
  children: [
808
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "text-xs font-medium", children: "Media" }),
809
- project.assets.length ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("ul", { className: "flex flex-col gap-1", "aria-label": "Media assets", children: project.assets.map((asset) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("li", { className: "flex items-center gap-2 text-xs", "data-asset": asset.id, children: [
810
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "rounded bg-muted px-1 text-[10px] uppercase text-muted-foreground", "data-asset-kind": "", children: asset.kind }),
811
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "min-w-0 flex-1 truncate", title: asset.src, children: asset.name ?? asset.id }),
812
- asset.duration !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "tabular-nums text-muted-foreground", children: (0, import_video_editor6.formatVideoSeconds)(asset.duration) }),
813
- editable && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: btn3, disabled: !canEdit, onClick: () => addToTimeline(asset), "data-add-to-timeline": "", children: "Add to timeline" })
814
- ] }, asset.id)) }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("p", { className: "text-xs text-muted-foreground", children: [
932
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "text-xs font-medium", children: "Media" }),
933
+ project.assets.length ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("ul", { className: "flex flex-col gap-1", "aria-label": "Media assets", children: project.assets.map((asset) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("li", { className: "flex items-center gap-2 text-xs", "data-asset": asset.id, children: [
934
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "rounded bg-muted px-1 text-[10px] uppercase text-muted-foreground", "data-asset-kind": "", children: asset.kind }),
935
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "min-w-0 flex-1 truncate", title: asset.src, children: asset.name ?? asset.id }),
936
+ asset.duration !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "tabular-nums text-muted-foreground", children: (0, import_video_editor7.formatVideoSeconds)(asset.duration) }),
937
+ editable && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: btn3, disabled: !canEdit, onClick: () => addToTimeline(asset), "data-add-to-timeline": "", children: "Add to timeline" })
938
+ ] }, asset.id)) }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "text-xs text-muted-foreground", children: [
815
939
  "No media yet.",
816
940
  editable ? " Choose or drop files, or add a URL." : ""
817
941
  ] }),
818
- editable && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
819
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { className: "flex flex-col gap-1 rounded border border-dashed border-border p-2 text-xs text-muted-foreground", children: [
942
+ editable && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
943
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { className: "flex flex-col gap-1 rounded border border-dashed border-border p-2 text-xs text-muted-foreground", children: [
820
944
  "Choose or drop video, audio or image files",
821
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
945
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
822
946
  "input",
823
947
  {
824
948
  type: "file",
@@ -835,7 +959,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
835
959
  }
836
960
  )
837
961
  ] }),
838
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
962
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
839
963
  "form",
840
964
  {
841
965
  className: "flex items-center gap-1",
@@ -844,7 +968,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
844
968
  void addUrl();
845
969
  },
846
970
  children: [
847
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
971
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
848
972
  "input",
849
973
  {
850
974
  className: "min-w-0 flex-1 rounded border border-border bg-transparent px-1 py-0.5 text-xs",
@@ -855,49 +979,49 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
855
979
  onChange: (e) => setUrl(e.target.value)
856
980
  }
857
981
  ),
858
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("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: [
859
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "", children: "Auto" }),
860
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "video", children: "Video" }),
861
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "audio", children: "Audio" }),
862
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "image", children: "Image" })
982
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("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: [
983
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: "", children: "Auto" }),
984
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: "video", children: "Video" }),
985
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: "audio", children: "Audio" }),
986
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: "image", children: "Image" })
863
987
  ] }),
864
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "submit", className: btn3, disabled: !canEdit || !url.trim(), "data-add-url": "", children: "Add" })
988
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "submit", className: btn3, disabled: !canEdit || !url.trim(), "data-add-url": "", children: "Add" })
865
989
  ]
866
990
  }
867
991
  )
868
992
  ] }),
869
- busy > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-xs text-muted-foreground", children: "Reading media\u2026" }),
870
- problem && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-xs text-red-500", role: "alert", "data-media-error": "", children: problem })
993
+ busy > 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "text-xs text-muted-foreground", children: "Reading media\u2026" }),
994
+ problem && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "text-xs text-red-500", role: "alert", "data-media-error": "", children: problem })
871
995
  ]
872
996
  }
873
997
  );
874
998
  }
875
999
 
876
1000
  // src/video-editor/TranscriptPanel.tsx
877
- var import_react5 = __toESM(require("react"), 1);
878
- var import_video_editor7 = require("@pixerate/editor/video-editor");
879
- var import_jsx_runtime6 = require("react/jsx-runtime");
1001
+ var import_react7 = __toESM(require("react"), 1);
1002
+ var import_video_editor8 = require("@pixerate/editor/video-editor");
1003
+ var import_jsx_runtime7 = require("react/jsx-runtime");
880
1004
  var btn4 = "rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40";
881
1005
  function TranscriptPanel({ controller, track, editable = true, className = "" }) {
882
1006
  const state = useVideoEditorState(controller);
883
1007
  const { project, time, readOnly } = state;
884
1008
  const canEdit = editable && !readOnly;
885
- const [anchor, setAnchor] = (0, import_react5.useState)(null);
886
- const [focus, setFocus] = (0, import_react5.useState)(null);
887
- const [message, setMessage] = (0, import_react5.useState)(null);
888
- const words = (0, import_react5.useMemo)(() => {
1009
+ const [anchor, setAnchor] = (0, import_react7.useState)(null);
1010
+ const [focus, setFocus] = (0, import_react7.useState)(null);
1011
+ const [message, setMessage] = (0, import_react7.useState)(null);
1012
+ const words = (0, import_react7.useMemo)(() => {
889
1013
  try {
890
- return (0, import_video_editor7.videoTimelineTranscript)(project, { track });
1014
+ return (0, import_video_editor8.videoTimelineTranscript)(project, { track });
891
1015
  } catch {
892
1016
  return [];
893
1017
  }
894
1018
  }, [project, track]);
895
- const current = (0, import_video_editor7.videoWordAt)(words, time);
1019
+ const current = (0, import_video_editor8.videoWordAt)(words, time);
896
1020
  const selected = anchor !== null && focus !== null && anchor < words.length && focus < words.length ? [Math.min(anchor, focus), Math.max(anchor, focus)] : null;
897
1021
  const sound = project.assets.filter((a) => a.kind !== "image");
898
1022
  const cut = (ranges, label, none) => {
899
1023
  if (!ranges.length) return setMessage(none);
900
- const r = controller.apply((0, import_video_editor7.videoRemoveRangesEdits)(ranges), label);
1024
+ const r = controller.apply((0, import_video_editor8.videoRemoveRangesEdits)(ranges), label);
901
1025
  setMessage(r.ok ? null : r.error);
902
1026
  setAnchor(null);
903
1027
  setFocus(null);
@@ -905,15 +1029,15 @@ function TranscriptPanel({ controller, track, editable = true, className = "" })
905
1029
  const load = async (assetId, file) => {
906
1030
  if (!file) return;
907
1031
  try {
908
- const r = controller.apply((0, import_video_editor7.videoTranscriptEdit)(assetId, await file.text()), "Attach transcript");
1032
+ const r = controller.apply((0, import_video_editor8.videoTranscriptEdit)(assetId, await file.text()), "Attach transcript");
909
1033
  setMessage(r.ok ? null : r.error);
910
1034
  } catch (e) {
911
1035
  setMessage(e instanceof Error ? e.message : String(e));
912
1036
  }
913
1037
  };
914
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: `flex flex-col gap-2 text-xs ${className}`, "data-transcript-panel": "", children: [
915
- words.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
916
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1038
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: `flex flex-col gap-2 text-xs ${className}`, "data-transcript-panel": "", children: [
1039
+ words.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
1040
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
917
1041
  "div",
918
1042
  {
919
1043
  className: "max-h-48 overflow-y-auto leading-6",
@@ -924,13 +1048,13 @@ function TranscriptPanel({ controller, track, editable = true, className = "" })
924
1048
  onKeyDown: (e) => {
925
1049
  if (canEdit && selected && (e.key === "Delete" || e.key === "Backspace")) {
926
1050
  e.preventDefault();
927
- cut([(0, import_video_editor7.videoWordsRange)(words, selected[0], selected[1])], "Cut words", "Select words to cut.");
1051
+ cut([(0, import_video_editor8.videoWordsRange)(words, selected[0], selected[1])], "Cut words", "Select words to cut.");
928
1052
  }
929
1053
  },
930
1054
  children: words.map((w, i) => {
931
1055
  const inSelection = selected !== null && i >= selected[0] && i <= selected[1];
932
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react5.default.Fragment, { children: [
933
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1056
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react7.default.Fragment, { children: [
1057
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
934
1058
  "span",
935
1059
  {
936
1060
  role: "option",
@@ -956,51 +1080,52 @@ function TranscriptPanel({ controller, track, editable = true, className = "" })
956
1080
  })
957
1081
  }
958
1082
  ),
959
- canEdit && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex flex-wrap gap-1", children: [
960
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: btn4, disabled: !selected, onClick: () => selected && cut([(0, import_video_editor7.videoWordsRange)(words, selected[0], selected[1])], "Cut words", "Select words to cut."), children: "Delete selected words" }),
961
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: btn4, onClick: () => cut((0, import_video_editor7.findVideoFillers)(words), "Remove fillers", "No filler words found."), children: "Remove fillers" }),
962
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: btn4, onClick: () => cut((0, import_video_editor7.findVideoTranscriptPauses)(words, { fps: project.fps }), "Tighten pauses", "No long pauses found."), children: "Tighten pauses" })
1083
+ canEdit && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex flex-wrap gap-1", children: [
1084
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: btn4, disabled: !selected, onClick: () => selected && cut([(0, import_video_editor8.videoWordsRange)(words, selected[0], selected[1])], "Cut words", "Select words to cut."), children: "Delete selected words" }),
1085
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: btn4, onClick: () => cut((0, import_video_editor8.findVideoFillers)(words), "Remove fillers", "No filler words found."), children: "Remove fillers" }),
1086
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: btn4, onClick: () => cut((0, import_video_editor8.findVideoTranscriptPauses)(words, { fps: project.fps }), "Tighten pauses", "No long pauses found."), children: "Tighten pauses" })
963
1087
  ] })
964
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("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." }),
965
- canEdit && sound.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("details", { children: [
966
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("summary", { className: "cursor-pointer text-muted-foreground", children: "Transcripts" }),
967
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("ul", { className: "mt-1 space-y-1", children: sound.map((a) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("li", { className: "flex items-center gap-2", children: [
968
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "min-w-0 flex-1 truncate", children: [
1088
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("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." }),
1089
+ canEdit && sound.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("details", { children: [
1090
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("summary", { className: "cursor-pointer text-muted-foreground", children: "Transcripts" }),
1091
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("ul", { className: "mt-1 space-y-1", children: sound.map((a) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("li", { className: "flex items-center gap-2", children: [
1092
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "min-w-0 flex-1 truncate", children: [
969
1093
  a.name ?? a.id,
970
1094
  " ",
971
1095
  a.transcript ? `\xB7 ${a.transcript.length} words` : ""
972
1096
  ] }),
973
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("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]) })
1097
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("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]) })
974
1098
  ] }, a.id)) })
975
1099
  ] }),
976
- message && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "text-red-500", role: "alert", "data-transcript-error": "", children: message })
1100
+ message && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "text-red-500", role: "alert", "data-transcript-error": "", children: message })
977
1101
  ] });
978
1102
  }
979
1103
 
980
1104
  // src/video-editor/VideoEditor.tsx
981
- var import_react6 = require("react");
982
- var import_jsx_runtime7 = require("react/jsx-runtime");
983
- function VideoEditor({ controller: given, options, editable = true, pxPerSecond, onPxPerSecondChange, resolution, showIssues = true, showTranscript = true, className = "" }) {
1105
+ var import_react8 = require("react");
1106
+ var import_jsx_runtime8 = require("react/jsx-runtime");
1107
+ function VideoEditor({ controller: given, options, editable = true, pxPerSecond, onPxPerSecondChange, resolution, showIssues = true, showTranscript = true, showScopes = false, className = "" }) {
984
1108
  const { controller, state } = useVideoEditor(given ?? options ?? {});
985
- (0, import_react6.useEffect)(() => {
1109
+ (0, import_react8.useEffect)(() => {
986
1110
  if (editable) return;
987
1111
  const was = controller.getState().readOnly;
988
1112
  controller.setReadOnly(true);
989
1113
  return () => controller.setReadOnly(was);
990
1114
  }, [controller, editable]);
991
1115
  const { issues } = state;
992
- const issueList = showIssues && issues.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("ul", { className: "space-y-0.5 text-xs", "aria-label": "Video issues", "data-video-issues": "", children: issues.map((issue, i) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("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)) });
993
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: `flex flex-col gap-3 ${className}`, "data-video-editor": "", children: [
994
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: `grid gap-3 ${editable ? "lg:grid-cols-[16rem_minmax(0,1fr)_18rem]" : ""}`, children: [
995
- editable && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(MediaBin, { controller, className: "min-w-0" }),
996
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex min-w-0 flex-col gap-2", children: [
997
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(VideoPreview, { controller, resolution, className: "rounded-md border border-border" }),
998
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(VideoTransport, { controller, editable })
1116
+ const issueList = showIssues && issues.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("ul", { className: "space-y-0.5 text-xs", "aria-label": "Video issues", "data-video-issues": "", children: issues.map((issue, i) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("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)) });
1117
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: `flex flex-col gap-3 ${className}`, "data-video-editor": "", children: [
1118
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: `grid gap-3 ${editable ? "lg:grid-cols-[16rem_minmax(0,1fr)_18rem]" : ""}`, children: [
1119
+ editable && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(MediaBin, { controller, className: "min-w-0" }),
1120
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex min-w-0 flex-col gap-2", children: [
1121
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(VideoPreview, { controller, resolution, className: "rounded-md border border-border" }),
1122
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(VideoTransport, { controller, editable }),
1123
+ showScopes && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(VideoScopes, { controller })
999
1124
  ] }),
1000
- editable && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ClipInspector, { controller, className: "min-w-0" })
1125
+ editable && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ClipInspector, { controller, className: "min-w-0" })
1001
1126
  ] }),
1002
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(VideoTimeline, { controller, editable, pxPerSecond, onPxPerSecondChange }),
1003
- showTranscript && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(TranscriptPanel, { controller, editable }),
1127
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(VideoTimeline, { controller, editable, pxPerSecond, onPxPerSecondChange }),
1128
+ showTranscript && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(TranscriptPanel, { controller, editable }),
1004
1129
  issueList
1005
1130
  ] });
1006
1131
  }
@@ -1011,6 +1136,7 @@ function VideoEditor({ controller: given, options, editable = true, pxPerSecond,
1011
1136
  TranscriptPanel,
1012
1137
  VideoEditor,
1013
1138
  VideoPreview,
1139
+ VideoScopes,
1014
1140
  VideoTimeline,
1015
1141
  VideoTransport,
1016
1142
  useVideoEditor,