@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.
@@ -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,17 @@ 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_video_editor6 = require("@pixerate/editor/video-editor");
522
+ var import_jsx_runtime5 = require("react/jsx-runtime");
454
523
  var btn2 = "rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40";
455
524
  var field = "w-full rounded border border-border bg-transparent px-1 py-0.5 text-xs disabled:opacity-50";
456
525
  var DEFAULT_TRANSITION = 500;
@@ -464,7 +533,7 @@ function Commit({
464
533
  if (raw !== value) onCommit(raw);
465
534
  };
466
535
  if (multiline)
467
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
536
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
468
537
  "textarea",
469
538
  {
470
539
  className: field,
@@ -476,11 +545,11 @@ function Commit({
476
545
  },
477
546
  value
478
547
  );
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);
548
+ 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);
480
549
  }
481
550
  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 }),
551
+ 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: [
552
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-muted-foreground", children: label }),
484
553
  children
485
554
  ] });
486
555
  }
@@ -488,8 +557,8 @@ function ClipInspector({ controller, className = "" }) {
488
557
  const state = useVideoEditorState(controller);
489
558
  const { project, selection, readOnly, time } = state;
490
559
  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]);
560
+ 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." });
561
+ const at = (0, import_video_editor6.findVideoClip)(project, selection[0]);
493
562
  if (!at) return null;
494
563
  const clip = at.clip;
495
564
  const disabled = readOnly || !!at.track.locked;
@@ -505,24 +574,24 @@ function ClipInspector({ controller, className = "" }) {
505
574
  };
506
575
  const text = (key, label) => {
507
576
  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}`) });
577
+ 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
578
  };
510
579
  const number = (key, label, attrs = {}) => {
511
580
  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 });
581
+ 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
582
  };
514
583
  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" })
584
+ 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: [
585
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center gap-2 text-xs", children: [
586
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "font-medium", children: clip.type === "media" ? "Media clip" : clip.type === "text" ? "Title" : "Colour clip" }),
587
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("code", { className: "text-muted-foreground", children: clip.id }),
588
+ at.track.locked && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-amber-500", children: "locked track" })
520
589
  ] }),
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)(
590
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Name", name: "name", children: text("name", "Name") }),
591
+ /* @__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) }) }),
592
+ /* @__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) }) }),
593
+ (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: [
594
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
526
595
  Commit,
527
596
  {
528
597
  type: "number",
@@ -536,12 +605,12 @@ function ClipInspector({ controller, className = "" }) {
536
605
  onCommit: (raw) => {
537
606
  const n = Number(raw);
538
607
  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);
608
+ const edit = (0, import_video_editor6.videoKeyframeValueEdit)(project, clip.id, row.property, time, raw.trim() ? n : void 0);
540
609
  if (edit) controller.apply(edit, row.keyed ? `Keyframe ${row.label.toLowerCase()}` : `Change ${row.label.toLowerCase()}`);
541
610
  }
542
611
  }
543
612
  ),
544
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
613
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
545
614
  "button",
546
615
  {
547
616
  type: "button",
@@ -552,17 +621,17 @@ function ClipInspector({ controller, className = "" }) {
552
621
  disabled,
553
622
  "data-keyframe-toggle": row.property,
554
623
  onClick: () => {
555
- const edit = (0, import_video_editor5.videoKeyframeToggleEdit)(project, clip.id, row.property, time);
624
+ const edit = (0, import_video_editor6.videoKeyframeToggleEdit)(project, clip.id, row.property, time);
556
625
  if (edit) controller.apply(edit, `${row.onKey ? "Remove" : "Add"} ${row.label.toLowerCase()} keyframe`);
557
626
  },
558
627
  children: row.onKey ? "\u25C6" : "\u25C7"
559
628
  }
560
629
  ),
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 })
630
+ 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
631
  ] }) }, 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)(
632
+ 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)) }) }),
633
+ at.track.kind === "video" && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
634
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Look", name: "gradePreset", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
566
635
  "select",
567
636
  {
568
637
  className: field,
@@ -570,16 +639,16 @@ function ClipInspector({ controller, className = "" }) {
570
639
  disabled,
571
640
  "aria-label": "Apply a look",
572
641
  onChange: (e) => {
573
- const grade = (0, import_video_editor5.videoGradePreset)(e.target.value);
642
+ const grade = (0, import_video_editor6.videoGradePreset)(e.target.value);
574
643
  if (grade) controller.apply({ op: "update_clip", clip: clip.id, set: { grade } }, `Apply ${e.target.value} look`);
575
644
  },
576
645
  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))
646
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "", children: clip.grade ? "Graded" : "None" }),
647
+ import_video_editor6.VIDEO_GRADE_PRESET_NAMES.map((name) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: name, children: name.replace(/-/g, " ") }, name))
579
648
  ]
580
649
  }
581
650
  ) }),
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)(
651
+ 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
652
  Commit,
584
653
  {
585
654
  type: "number",
@@ -593,13 +662,13 @@ function ClipInspector({ controller, className = "" }) {
593
662
  onCommit: (raw) => {
594
663
  const n = raw.trim() ? Number(raw) : 0;
595
664
  if (!Number.isFinite(n)) return;
596
- const grade = (0, import_video_editor5.normalizeVideoGrade)({ ...clip.grade, [c.key]: n });
665
+ const grade = (0, import_video_editor6.normalizeVideoGrade)({ ...clip.grade, [c.key]: n });
597
666
  set("grade", grade, `Change ${c.label.toLowerCase()}`);
598
667
  }
599
668
  }
600
669
  ) }, c.key))
601
670
  ] }),
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)(
671
+ 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
672
  "select",
604
673
  {
605
674
  className: field,
@@ -611,35 +680,35 @@ function ClipInspector({ controller, className = "" }) {
611
680
  set(key, effect ? { ...clip[key], effect } : void 0, key === "animateIn" ? "Change entrance" : "Change exit");
612
681
  },
613
682
  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))
683
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "", children: "None" }),
684
+ 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
685
  ]
617
686
  }
618
687
  ) }, 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" })
688
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Fade in (ms)", name: "fadeIn", children: number("fadeIn", "Fade in", { min: 0, step: 100 }) }),
689
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Fade out (ms)", name: "fadeOut", children: number("fadeOut", "Fade out", { min: 0, step: 100 }) }),
690
+ clip.type === "media" && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
691
+ /* @__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: [
692
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-muted-foreground", children: "ramped" }),
693
+ /* @__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
694
  ] }) : 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)(
695
+ 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: [
696
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
628
697
  "button",
629
698
  {
630
699
  type: "button",
631
700
  className: btn2,
632
701
  disabled: disabled || time < clip.start || time > clip.start + clip.duration,
633
702
  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"),
703
+ onClick: () => controller.apply((0, import_video_editor6.videoFreezeFrameEdit)(clip, time - clip.start, 1e3), "Freeze frame"),
635
704
  children: "Freeze 1 s here"
636
705
  }
637
706
  ),
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" })
707
+ /* @__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
708
  ] }) }),
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") }) }),
709
+ /* @__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
710
  ["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)(
711
+ (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
712
  "select",
644
713
  {
645
714
  className: field,
@@ -650,19 +719,19 @@ function ClipInspector({ controller, className = "" }) {
650
719
  const curve = e.target.value;
651
720
  set(key, curve === "linear" ? void 0 : curve, key === "fadeInCurve" ? "Change fade in curve" : "Change fade out curve");
652
721
  },
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))
722
+ 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
723
  }
655
724
  ) }, key) : null
656
725
  )
657
726
  ] }),
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") })
727
+ clip.type === "text" && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
728
+ /* @__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") }) }),
729
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Colour", name: "color", children: text("color", "Colour") }),
730
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Font size", name: "fontSize", children: number("fontSize", "Font size", { min: 1, step: 2 }) }),
731
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Background", name: "background", children: text("background", "Background") })
663
732
  ] }),
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)(
733
+ 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") }) }),
734
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Transition in", name: "transition", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
666
735
  "select",
667
736
  {
668
737
  className: field,
@@ -677,12 +746,12 @@ function ClipInspector({ controller, className = "" }) {
677
746
  );
678
747
  },
679
748
  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))
749
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "", children: "None" }),
750
+ 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
751
  ]
683
752
  }
684
753
  ) }),
685
- transition && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Length (ms)", name: "transitionDuration", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
754
+ transition && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Length (ms)", name: "transitionDuration", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
686
755
  Commit,
687
756
  {
688
757
  type: "number",
@@ -697,7 +766,7 @@ function ClipInspector({ controller, className = "" }) {
697
766
  }
698
767
  }
699
768
  ) }),
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)(
769
+ 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
770
  "select",
702
771
  {
703
772
  className: field,
@@ -705,41 +774,41 @@ function ClipInspector({ controller, className = "" }) {
705
774
  disabled,
706
775
  "aria-label": "Transition direction",
707
776
  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))
777
+ children: import_video_editor6.VIDEO_WIPE_DIRECTIONS.map((d) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: d, children: d }, d))
709
778
  }
710
779
  ) }),
711
- transition && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Transition sound", name: "transitionAudioCurve", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
780
+ transition && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Transition sound", name: "transitionAudioCurve", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
712
781
  "select",
713
782
  {
714
783
  className: field,
715
- value: (0, import_video_editor5.videoTransitionAudioCurve)(transition),
784
+ value: (0, import_video_editor6.videoTransitionAudioCurve)(transition),
716
785
  disabled,
717
786
  "aria-label": "Transition sound curve",
718
787
  onChange: (e) => {
719
788
  const { audioCurve: _, ...rest } = transition;
720
789
  const curve = e.target.value;
721
- const next = curve === (0, import_video_editor5.videoTransitionAudioCurve)(rest) ? rest : { ...rest, audioCurve: curve };
790
+ const next = curve === (0, import_video_editor6.videoTransitionAudioCurve)(rest) ? rest : { ...rest, audioCurve: curve };
722
791
  controller.apply({ op: "set_transition", clip: clip.id, transition: next }, "Change transition sound");
723
792
  },
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))
793
+ 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
794
  }
726
795
  ) })
727
796
  ] });
728
797
  }
729
798
 
730
799
  // 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");
800
+ var import_react5 = require("react");
801
+ var import_video_editor7 = require("@pixerate/editor/video-editor");
802
+ var import_jsx_runtime6 = require("react/jsx-runtime");
734
803
  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 = "" }) {
804
+ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video_editor7.probeVideoMedia, className = "" }) {
736
805
  const { project, readOnly } = useVideoEditorState(controller);
737
806
  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);
807
+ const [url, setUrl] = (0, import_react5.useState)("");
808
+ const [urlKind, setUrlKind] = (0, import_react5.useState)("");
809
+ const [busy, setBusy] = (0, import_react5.useState)(0);
810
+ const [problem, setProblem] = (0, import_react5.useState)(null);
811
+ const [over, setOver] = (0, import_react5.useState)(false);
743
812
  const addSource = async (src, kind, name) => {
744
813
  setBusy((n) => n + 1);
745
814
  try {
@@ -753,7 +822,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
753
822
  const addFiles = async (files) => {
754
823
  setProblem(null);
755
824
  for (const file of Array.from(files ?? [])) {
756
- const kind = (0, import_video_editor6.videoAssetKindOf)(file.type || file.name);
825
+ const kind = (0, import_video_editor7.videoAssetKindOf)(file.type || file.name);
757
826
  if (!kind) {
758
827
  setProblem(`"${file.name}" is not a video, audio or image file.`);
759
828
  continue;
@@ -765,7 +834,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
765
834
  const addUrl = async () => {
766
835
  const src = url.trim();
767
836
  if (!src) return;
768
- const kind = urlKind || (0, import_video_editor6.videoAssetKindOf)(src.replace(/[?#].*$/, ""));
837
+ const kind = urlKind || (0, import_video_editor7.videoAssetKindOf)(src.replace(/[?#].*$/, ""));
769
838
  if (!kind) {
770
839
  setProblem(`Can't tell whether "${src}" is video, audio or an image; choose its kind.`);
771
840
  return;
@@ -775,7 +844,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
775
844
  setUrl("");
776
845
  };
777
846
  const addToTimeline = (asset) => {
778
- const edit = (0, import_video_editor6.videoAssetDropEdit)(project, asset.id);
847
+ const edit = (0, import_video_editor7.videoAssetDropEdit)(project, asset.id);
779
848
  if (!edit) {
780
849
  setProblem(`There is no unlocked ${asset.kind === "audio" ? "audio" : "video"} track to put "${asset.name ?? asset.id}" on.`);
781
850
  return;
@@ -787,7 +856,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
787
856
  if (added) controller.select([added]);
788
857
  }
789
858
  };
790
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
859
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
791
860
  "div",
792
861
  {
793
862
  className: `flex flex-col gap-2 rounded ${over ? "ring-2 ring-rose-500/60" : ""} ${className}`,
@@ -805,20 +874,20 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
805
874
  void addFiles(e.dataTransfer.files);
806
875
  },
807
876
  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: [
877
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "text-xs font-medium", children: "Media" }),
878
+ 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: [
879
+ /* @__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 }),
880
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "min-w-0 flex-1 truncate", title: asset.src, children: asset.name ?? asset.id }),
881
+ 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) }),
882
+ 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" })
883
+ ] }, asset.id)) }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "text-xs text-muted-foreground", children: [
815
884
  "No media yet.",
816
885
  editable ? " Choose or drop files, or add a URL." : ""
817
886
  ] }),
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: [
887
+ editable && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
888
+ /* @__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
889
  "Choose or drop video, audio or image files",
821
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
890
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
822
891
  "input",
823
892
  {
824
893
  type: "file",
@@ -835,7 +904,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
835
904
  }
836
905
  )
837
906
  ] }),
838
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
907
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
839
908
  "form",
840
909
  {
841
910
  className: "flex items-center gap-1",
@@ -844,7 +913,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
844
913
  void addUrl();
845
914
  },
846
915
  children: [
847
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
916
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
848
917
  "input",
849
918
  {
850
919
  className: "min-w-0 flex-1 rounded border border-border bg-transparent px-1 py-0.5 text-xs",
@@ -855,49 +924,49 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
855
924
  onChange: (e) => setUrl(e.target.value)
856
925
  }
857
926
  ),
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" })
927
+ /* @__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: [
928
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: "", children: "Auto" }),
929
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: "video", children: "Video" }),
930
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: "audio", children: "Audio" }),
931
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: "image", children: "Image" })
863
932
  ] }),
864
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "submit", className: btn3, disabled: !canEdit || !url.trim(), "data-add-url": "", children: "Add" })
933
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "submit", className: btn3, disabled: !canEdit || !url.trim(), "data-add-url": "", children: "Add" })
865
934
  ]
866
935
  }
867
936
  )
868
937
  ] }),
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 })
938
+ busy > 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "text-xs text-muted-foreground", children: "Reading media\u2026" }),
939
+ problem && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "text-xs text-red-500", role: "alert", "data-media-error": "", children: problem })
871
940
  ]
872
941
  }
873
942
  );
874
943
  }
875
944
 
876
945
  // 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");
946
+ var import_react6 = __toESM(require("react"), 1);
947
+ var import_video_editor8 = require("@pixerate/editor/video-editor");
948
+ var import_jsx_runtime7 = require("react/jsx-runtime");
880
949
  var btn4 = "rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40";
881
950
  function TranscriptPanel({ controller, track, editable = true, className = "" }) {
882
951
  const state = useVideoEditorState(controller);
883
952
  const { project, time, readOnly } = state;
884
953
  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)(() => {
954
+ const [anchor, setAnchor] = (0, import_react6.useState)(null);
955
+ const [focus, setFocus] = (0, import_react6.useState)(null);
956
+ const [message, setMessage] = (0, import_react6.useState)(null);
957
+ const words = (0, import_react6.useMemo)(() => {
889
958
  try {
890
- return (0, import_video_editor7.videoTimelineTranscript)(project, { track });
959
+ return (0, import_video_editor8.videoTimelineTranscript)(project, { track });
891
960
  } catch {
892
961
  return [];
893
962
  }
894
963
  }, [project, track]);
895
- const current = (0, import_video_editor7.videoWordAt)(words, time);
964
+ const current = (0, import_video_editor8.videoWordAt)(words, time);
896
965
  const selected = anchor !== null && focus !== null && anchor < words.length && focus < words.length ? [Math.min(anchor, focus), Math.max(anchor, focus)] : null;
897
966
  const sound = project.assets.filter((a) => a.kind !== "image");
898
967
  const cut = (ranges, label, none) => {
899
968
  if (!ranges.length) return setMessage(none);
900
- const r = controller.apply((0, import_video_editor7.videoRemoveRangesEdits)(ranges), label);
969
+ const r = controller.apply((0, import_video_editor8.videoRemoveRangesEdits)(ranges), label);
901
970
  setMessage(r.ok ? null : r.error);
902
971
  setAnchor(null);
903
972
  setFocus(null);
@@ -905,15 +974,15 @@ function TranscriptPanel({ controller, track, editable = true, className = "" })
905
974
  const load = async (assetId, file) => {
906
975
  if (!file) return;
907
976
  try {
908
- const r = controller.apply((0, import_video_editor7.videoTranscriptEdit)(assetId, await file.text()), "Attach transcript");
977
+ const r = controller.apply((0, import_video_editor8.videoTranscriptEdit)(assetId, await file.text()), "Attach transcript");
909
978
  setMessage(r.ok ? null : r.error);
910
979
  } catch (e) {
911
980
  setMessage(e instanceof Error ? e.message : String(e));
912
981
  }
913
982
  };
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)(
983
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: `flex flex-col gap-2 text-xs ${className}`, "data-transcript-panel": "", children: [
984
+ words.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
985
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
917
986
  "div",
918
987
  {
919
988
  className: "max-h-48 overflow-y-auto leading-6",
@@ -924,13 +993,13 @@ function TranscriptPanel({ controller, track, editable = true, className = "" })
924
993
  onKeyDown: (e) => {
925
994
  if (canEdit && selected && (e.key === "Delete" || e.key === "Backspace")) {
926
995
  e.preventDefault();
927
- cut([(0, import_video_editor7.videoWordsRange)(words, selected[0], selected[1])], "Cut words", "Select words to cut.");
996
+ cut([(0, import_video_editor8.videoWordsRange)(words, selected[0], selected[1])], "Cut words", "Select words to cut.");
928
997
  }
929
998
  },
930
999
  children: words.map((w, i) => {
931
1000
  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)(
1001
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react6.default.Fragment, { children: [
1002
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
934
1003
  "span",
935
1004
  {
936
1005
  role: "option",
@@ -956,51 +1025,52 @@ function TranscriptPanel({ controller, track, editable = true, className = "" })
956
1025
  })
957
1026
  }
958
1027
  ),
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" })
1028
+ canEdit && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex flex-wrap gap-1", children: [
1029
+ /* @__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" }),
1030
+ /* @__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" }),
1031
+ /* @__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
1032
  ] })
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: [
1033
+ ] }) : /* @__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." }),
1034
+ canEdit && sound.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("details", { children: [
1035
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("summary", { className: "cursor-pointer text-muted-foreground", children: "Transcripts" }),
1036
+ /* @__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: [
1037
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "min-w-0 flex-1 truncate", children: [
969
1038
  a.name ?? a.id,
970
1039
  " ",
971
1040
  a.transcript ? `\xB7 ${a.transcript.length} words` : ""
972
1041
  ] }),
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]) })
1042
+ /* @__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
1043
  ] }, a.id)) })
975
1044
  ] }),
976
- message && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "text-red-500", role: "alert", "data-transcript-error": "", children: message })
1045
+ message && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "text-red-500", role: "alert", "data-transcript-error": "", children: message })
977
1046
  ] });
978
1047
  }
979
1048
 
980
1049
  // 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 = "" }) {
1050
+ var import_react7 = require("react");
1051
+ var import_jsx_runtime8 = require("react/jsx-runtime");
1052
+ function VideoEditor({ controller: given, options, editable = true, pxPerSecond, onPxPerSecondChange, resolution, showIssues = true, showTranscript = true, showScopes = false, className = "" }) {
984
1053
  const { controller, state } = useVideoEditor(given ?? options ?? {});
985
- (0, import_react6.useEffect)(() => {
1054
+ (0, import_react7.useEffect)(() => {
986
1055
  if (editable) return;
987
1056
  const was = controller.getState().readOnly;
988
1057
  controller.setReadOnly(true);
989
1058
  return () => controller.setReadOnly(was);
990
1059
  }, [controller, editable]);
991
1060
  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 })
1061
+ 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)) });
1062
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: `flex flex-col gap-3 ${className}`, "data-video-editor": "", children: [
1063
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: `grid gap-3 ${editable ? "lg:grid-cols-[16rem_minmax(0,1fr)_18rem]" : ""}`, children: [
1064
+ editable && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(MediaBin, { controller, className: "min-w-0" }),
1065
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex min-w-0 flex-col gap-2", children: [
1066
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(VideoPreview, { controller, resolution, className: "rounded-md border border-border" }),
1067
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(VideoTransport, { controller, editable }),
1068
+ showScopes && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(VideoScopes, { controller })
999
1069
  ] }),
1000
- editable && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ClipInspector, { controller, className: "min-w-0" })
1070
+ editable && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ClipInspector, { controller, className: "min-w-0" })
1001
1071
  ] }),
1002
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(VideoTimeline, { controller, editable, pxPerSecond, onPxPerSecondChange }),
1003
- showTranscript && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(TranscriptPanel, { controller, editable }),
1072
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(VideoTimeline, { controller, editable, pxPerSecond, onPxPerSecondChange }),
1073
+ showTranscript && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(TranscriptPanel, { controller, editable }),
1004
1074
  issueList
1005
1075
  ] });
1006
1076
  }
@@ -1011,6 +1081,7 @@ function VideoEditor({ controller: given, options, editable = true, pxPerSecond,
1011
1081
  TranscriptPanel,
1012
1082
  VideoEditor,
1013
1083
  VideoPreview,
1084
+ VideoScopes,
1014
1085
  VideoTimeline,
1015
1086
  VideoTransport,
1016
1087
  useVideoEditor,