@pixerate/editor-react 0.55.0 → 0.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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_video_editor6 = require("@pixerate/editor/video-editor");
522
+ var import_jsx_runtime5 = require("react/jsx-runtime");
523
+ var btn2 = "rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40";
454
524
  var field = "w-full rounded border border-border bg-transparent px-1 py-0.5 text-xs disabled:opacity-50";
455
525
  var DEFAULT_TRANSITION = 500;
456
526
  function Commit({
@@ -463,7 +533,7 @@ function Commit({
463
533
  if (raw !== value) onCommit(raw);
464
534
  };
465
535
  if (multiline)
466
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
536
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
467
537
  "textarea",
468
538
  {
469
539
  className: field,
@@ -475,11 +545,11 @@ function Commit({
475
545
  },
476
546
  value
477
547
  );
478
- 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);
479
549
  }
480
550
  function Row({ label, name, children }) {
481
- 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: [
482
- /* @__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 }),
483
553
  children
484
554
  ] });
485
555
  }
@@ -487,8 +557,8 @@ function ClipInspector({ controller, className = "" }) {
487
557
  const state = useVideoEditorState(controller);
488
558
  const { project, selection, readOnly, time } = state;
489
559
  if (selection.length !== 1)
490
- 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." });
491
- 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]);
492
562
  if (!at) return null;
493
563
  const clip = at.clip;
494
564
  const disabled = readOnly || !!at.track.locked;
@@ -504,24 +574,24 @@ function ClipInspector({ controller, className = "" }) {
504
574
  };
505
575
  const text = (key, label) => {
506
576
  const value = clip[key];
507
- 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}`) });
508
578
  };
509
579
  const number = (key, label, attrs = {}) => {
510
580
  const value = clip[key];
511
- 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 });
512
582
  };
513
583
  const transition = clip.transition;
514
- 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: [
515
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex items-center gap-2 text-xs", children: [
516
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "font-medium", children: clip.type === "media" ? "Media clip" : clip.type === "text" ? "Title" : "Colour clip" }),
517
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("code", { className: "text-muted-foreground", children: clip.id }),
518
- 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" })
519
589
  ] }),
520
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Name", name: "name", children: text("name", "Name") }),
521
- /* @__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) }) }),
522
- /* @__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) }) }),
523
- (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: [
524
- /* @__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)(
525
595
  Commit,
526
596
  {
527
597
  type: "number",
@@ -535,12 +605,12 @@ function ClipInspector({ controller, className = "" }) {
535
605
  onCommit: (raw) => {
536
606
  const n = Number(raw);
537
607
  if (raw.trim() && !Number.isFinite(n)) return;
538
- 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);
539
609
  if (edit) controller.apply(edit, row.keyed ? `Keyframe ${row.label.toLowerCase()}` : `Change ${row.label.toLowerCase()}`);
540
610
  }
541
611
  }
542
612
  ),
543
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
613
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
544
614
  "button",
545
615
  {
546
616
  type: "button",
@@ -551,17 +621,17 @@ function ClipInspector({ controller, className = "" }) {
551
621
  disabled,
552
622
  "data-keyframe-toggle": row.property,
553
623
  onClick: () => {
554
- 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);
555
625
  if (edit) controller.apply(edit, `${row.onKey ? "Remove" : "Add"} ${row.label.toLowerCase()} keyframe`);
556
626
  },
557
627
  children: row.onKey ? "\u25C6" : "\u25C7"
558
628
  }
559
629
  ),
560
- 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 })
561
631
  ] }) }, row.property)),
562
- 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)) }) }),
563
- at.track.kind === "video" && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
564
- /* @__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)(
565
635
  "select",
566
636
  {
567
637
  className: field,
@@ -569,16 +639,16 @@ function ClipInspector({ controller, className = "" }) {
569
639
  disabled,
570
640
  "aria-label": "Apply a look",
571
641
  onChange: (e) => {
572
- const grade = (0, import_video_editor5.videoGradePreset)(e.target.value);
642
+ const grade = (0, import_video_editor6.videoGradePreset)(e.target.value);
573
643
  if (grade) controller.apply({ op: "update_clip", clip: clip.id, set: { grade } }, `Apply ${e.target.value} look`);
574
644
  },
575
645
  children: [
576
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: "", children: clip.grade ? "Graded" : "None" }),
577
- 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))
578
648
  ]
579
649
  }
580
650
  ) }),
581
- 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)(
582
652
  Commit,
583
653
  {
584
654
  type: "number",
@@ -592,13 +662,13 @@ function ClipInspector({ controller, className = "" }) {
592
662
  onCommit: (raw) => {
593
663
  const n = raw.trim() ? Number(raw) : 0;
594
664
  if (!Number.isFinite(n)) return;
595
- 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 });
596
666
  set("grade", grade, `Change ${c.label.toLowerCase()}`);
597
667
  }
598
668
  }
599
669
  ) }, c.key))
600
670
  ] }),
601
- 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)(
602
672
  "select",
603
673
  {
604
674
  className: field,
@@ -610,18 +680,35 @@ function ClipInspector({ controller, className = "" }) {
610
680
  set(key, effect ? { ...clip[key], effect } : void 0, key === "animateIn" ? "Change entrance" : "Change exit");
611
681
  },
612
682
  children: [
613
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: "", children: "None" }),
614
- 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))
615
685
  ]
616
686
  }
617
687
  ) }, key)),
618
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Fade in (ms)", name: "fadeIn", children: number("fadeIn", "Fade in", { min: 0, step: 100 }) }),
619
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Fade out (ms)", name: "fadeOut", children: number("fadeOut", "Fade out", { min: 0, step: 100 }) }),
620
- clip.type === "media" && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
621
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Speed", name: "speed", children: number("speed", "Speed", { min: 0.1, step: 0.25 }) }),
622
- /* @__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") }) }),
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" })
694
+ ] }) : number("speed", "Speed", { min: 0.1, step: 0.25 }) }),
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)(
697
+ "button",
698
+ {
699
+ type: "button",
700
+ className: btn2,
701
+ disabled: disabled || time < clip.start || time > clip.start + clip.duration,
702
+ title: "Hold the frame at the playhead for a second, pushing later clips along",
703
+ onClick: () => controller.apply((0, import_video_editor6.videoFreezeFrameEdit)(clip, time - clip.start, 1e3), "Freeze frame"),
704
+ children: "Freeze 1 s here"
705
+ }
706
+ ),
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" })
708
+ ] }) }),
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") }) }),
623
710
  ["fadeInCurve", "fadeOutCurve"].map(
624
- (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)(
625
712
  "select",
626
713
  {
627
714
  className: field,
@@ -632,19 +719,19 @@ function ClipInspector({ controller, className = "" }) {
632
719
  const curve = e.target.value;
633
720
  set(key, curve === "linear" ? void 0 : curve, key === "fadeInCurve" ? "Change fade in curve" : "Change fade out curve");
634
721
  },
635
- 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))
636
723
  }
637
724
  ) }, key) : null
638
725
  )
639
726
  ] }),
640
- clip.type === "text" && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
641
- /* @__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") }) }),
642
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Colour", name: "color", children: text("color", "Colour") }),
643
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Row, { label: "Font size", name: "fontSize", children: number("fontSize", "Font size", { min: 1, step: 2 }) }),
644
- /* @__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") })
645
732
  ] }),
646
- 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") }) }),
647
- /* @__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)(
648
735
  "select",
649
736
  {
650
737
  className: field,
@@ -659,12 +746,12 @@ function ClipInspector({ controller, className = "" }) {
659
746
  );
660
747
  },
661
748
  children: [
662
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: "", children: "None" }),
663
- 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))
664
751
  ]
665
752
  }
666
753
  ) }),
667
- 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)(
668
755
  Commit,
669
756
  {
670
757
  type: "number",
@@ -679,7 +766,7 @@ function ClipInspector({ controller, className = "" }) {
679
766
  }
680
767
  }
681
768
  ) }),
682
- 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)(
683
770
  "select",
684
771
  {
685
772
  className: field,
@@ -687,41 +774,41 @@ function ClipInspector({ controller, className = "" }) {
687
774
  disabled,
688
775
  "aria-label": "Transition direction",
689
776
  onChange: (e) => controller.apply({ op: "set_transition", clip: clip.id, transition: { ...transition, direction: e.target.value } }, "Change transition direction"),
690
- 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))
691
778
  }
692
779
  ) }),
693
- 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)(
694
781
  "select",
695
782
  {
696
783
  className: field,
697
- value: (0, import_video_editor5.videoTransitionAudioCurve)(transition),
784
+ value: (0, import_video_editor6.videoTransitionAudioCurve)(transition),
698
785
  disabled,
699
786
  "aria-label": "Transition sound curve",
700
787
  onChange: (e) => {
701
788
  const { audioCurve: _, ...rest } = transition;
702
789
  const curve = e.target.value;
703
- 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 };
704
791
  controller.apply({ op: "set_transition", clip: clip.id, transition: next }, "Change transition sound");
705
792
  },
706
- 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))
707
794
  }
708
795
  ) })
709
796
  ] });
710
797
  }
711
798
 
712
799
  // src/video-editor/MediaBin.tsx
713
- var import_react4 = require("react");
714
- var import_video_editor6 = require("@pixerate/editor/video-editor");
715
- var import_jsx_runtime5 = require("react/jsx-runtime");
716
- var btn2 = "rounded border border-border px-2 py-0.5 text-xs disabled:opacity-40";
717
- function MediaBin({ controller, editable = true, fileToUrl, probe = import_video_editor6.probeVideoMedia, className = "" }) {
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");
803
+ var btn3 = "rounded border border-border px-2 py-0.5 text-xs disabled:opacity-40";
804
+ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video_editor7.probeVideoMedia, className = "" }) {
718
805
  const { project, readOnly } = useVideoEditorState(controller);
719
806
  const canEdit = editable && !readOnly;
720
- const [url, setUrl] = (0, import_react4.useState)("");
721
- const [urlKind, setUrlKind] = (0, import_react4.useState)("");
722
- const [busy, setBusy] = (0, import_react4.useState)(0);
723
- const [problem, setProblem] = (0, import_react4.useState)(null);
724
- 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);
725
812
  const addSource = async (src, kind, name) => {
726
813
  setBusy((n) => n + 1);
727
814
  try {
@@ -735,7 +822,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
735
822
  const addFiles = async (files) => {
736
823
  setProblem(null);
737
824
  for (const file of Array.from(files ?? [])) {
738
- const kind = (0, import_video_editor6.videoAssetKindOf)(file.type || file.name);
825
+ const kind = (0, import_video_editor7.videoAssetKindOf)(file.type || file.name);
739
826
  if (!kind) {
740
827
  setProblem(`"${file.name}" is not a video, audio or image file.`);
741
828
  continue;
@@ -747,7 +834,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
747
834
  const addUrl = async () => {
748
835
  const src = url.trim();
749
836
  if (!src) return;
750
- const kind = urlKind || (0, import_video_editor6.videoAssetKindOf)(src.replace(/[?#].*$/, ""));
837
+ const kind = urlKind || (0, import_video_editor7.videoAssetKindOf)(src.replace(/[?#].*$/, ""));
751
838
  if (!kind) {
752
839
  setProblem(`Can't tell whether "${src}" is video, audio or an image; choose its kind.`);
753
840
  return;
@@ -757,7 +844,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
757
844
  setUrl("");
758
845
  };
759
846
  const addToTimeline = (asset) => {
760
- const edit = (0, import_video_editor6.videoAssetDropEdit)(project, asset.id);
847
+ const edit = (0, import_video_editor7.videoAssetDropEdit)(project, asset.id);
761
848
  if (!edit) {
762
849
  setProblem(`There is no unlocked ${asset.kind === "audio" ? "audio" : "video"} track to put "${asset.name ?? asset.id}" on.`);
763
850
  return;
@@ -769,7 +856,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
769
856
  if (added) controller.select([added]);
770
857
  }
771
858
  };
772
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
859
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
773
860
  "div",
774
861
  {
775
862
  className: `flex flex-col gap-2 rounded ${over ? "ring-2 ring-rose-500/60" : ""} ${className}`,
@@ -787,20 +874,20 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
787
874
  void addFiles(e.dataTransfer.files);
788
875
  },
789
876
  children: [
790
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "text-xs font-medium", children: "Media" }),
791
- 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: [
792
- /* @__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 }),
793
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "min-w-0 flex-1 truncate", title: asset.src, children: asset.name ?? asset.id }),
794
- 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) }),
795
- editable && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: btn2, disabled: !canEdit, onClick: () => addToTimeline(asset), "data-add-to-timeline": "", children: "Add to timeline" })
796
- ] }, 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: [
797
884
  "No media yet.",
798
885
  editable ? " Choose or drop files, or add a URL." : ""
799
886
  ] }),
800
- editable && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
801
- /* @__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: [
802
889
  "Choose or drop video, audio or image files",
803
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
890
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
804
891
  "input",
805
892
  {
806
893
  type: "file",
@@ -817,7 +904,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
817
904
  }
818
905
  )
819
906
  ] }),
820
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
907
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
821
908
  "form",
822
909
  {
823
910
  className: "flex items-center gap-1",
@@ -826,7 +913,7 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
826
913
  void addUrl();
827
914
  },
828
915
  children: [
829
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
916
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
830
917
  "input",
831
918
  {
832
919
  className: "min-w-0 flex-1 rounded border border-border bg-transparent px-1 py-0.5 text-xs",
@@ -837,49 +924,49 @@ function MediaBin({ controller, editable = true, fileToUrl, probe = import_video
837
924
  onChange: (e) => setUrl(e.target.value)
838
925
  }
839
926
  ),
840
- /* @__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: [
841
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "", children: "Auto" }),
842
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "video", children: "Video" }),
843
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "audio", children: "Audio" }),
844
- /* @__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" })
845
932
  ] }),
846
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "submit", className: btn2, 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" })
847
934
  ]
848
935
  }
849
936
  )
850
937
  ] }),
851
- busy > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-xs text-muted-foreground", children: "Reading media\u2026" }),
852
- 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 })
853
940
  ]
854
941
  }
855
942
  );
856
943
  }
857
944
 
858
945
  // src/video-editor/TranscriptPanel.tsx
859
- var import_react5 = __toESM(require("react"), 1);
860
- var import_video_editor7 = require("@pixerate/editor/video-editor");
861
- var import_jsx_runtime6 = require("react/jsx-runtime");
862
- var btn3 = "rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40";
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");
949
+ var btn4 = "rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40";
863
950
  function TranscriptPanel({ controller, track, editable = true, className = "" }) {
864
951
  const state = useVideoEditorState(controller);
865
952
  const { project, time, readOnly } = state;
866
953
  const canEdit = editable && !readOnly;
867
- const [anchor, setAnchor] = (0, import_react5.useState)(null);
868
- const [focus, setFocus] = (0, import_react5.useState)(null);
869
- const [message, setMessage] = (0, import_react5.useState)(null);
870
- 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)(() => {
871
958
  try {
872
- return (0, import_video_editor7.videoTimelineTranscript)(project, { track });
959
+ return (0, import_video_editor8.videoTimelineTranscript)(project, { track });
873
960
  } catch {
874
961
  return [];
875
962
  }
876
963
  }, [project, track]);
877
- const current = (0, import_video_editor7.videoWordAt)(words, time);
964
+ const current = (0, import_video_editor8.videoWordAt)(words, time);
878
965
  const selected = anchor !== null && focus !== null && anchor < words.length && focus < words.length ? [Math.min(anchor, focus), Math.max(anchor, focus)] : null;
879
966
  const sound = project.assets.filter((a) => a.kind !== "image");
880
967
  const cut = (ranges, label, none) => {
881
968
  if (!ranges.length) return setMessage(none);
882
- const r = controller.apply((0, import_video_editor7.videoRemoveRangesEdits)(ranges), label);
969
+ const r = controller.apply((0, import_video_editor8.videoRemoveRangesEdits)(ranges), label);
883
970
  setMessage(r.ok ? null : r.error);
884
971
  setAnchor(null);
885
972
  setFocus(null);
@@ -887,15 +974,15 @@ function TranscriptPanel({ controller, track, editable = true, className = "" })
887
974
  const load = async (assetId, file) => {
888
975
  if (!file) return;
889
976
  try {
890
- 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");
891
978
  setMessage(r.ok ? null : r.error);
892
979
  } catch (e) {
893
980
  setMessage(e instanceof Error ? e.message : String(e));
894
981
  }
895
982
  };
896
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: `flex flex-col gap-2 text-xs ${className}`, "data-transcript-panel": "", children: [
897
- words.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
898
- /* @__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)(
899
986
  "div",
900
987
  {
901
988
  className: "max-h-48 overflow-y-auto leading-6",
@@ -906,13 +993,13 @@ function TranscriptPanel({ controller, track, editable = true, className = "" })
906
993
  onKeyDown: (e) => {
907
994
  if (canEdit && selected && (e.key === "Delete" || e.key === "Backspace")) {
908
995
  e.preventDefault();
909
- 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.");
910
997
  }
911
998
  },
912
999
  children: words.map((w, i) => {
913
1000
  const inSelection = selected !== null && i >= selected[0] && i <= selected[1];
914
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react5.default.Fragment, { children: [
915
- /* @__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)(
916
1003
  "span",
917
1004
  {
918
1005
  role: "option",
@@ -938,51 +1025,52 @@ function TranscriptPanel({ controller, track, editable = true, className = "" })
938
1025
  })
939
1026
  }
940
1027
  ),
941
- canEdit && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex flex-wrap gap-1", children: [
942
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: btn3, 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" }),
943
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: btn3, onClick: () => cut((0, import_video_editor7.findVideoFillers)(words), "Remove fillers", "No filler words found."), children: "Remove fillers" }),
944
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: btn3, 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" })
945
1032
  ] })
946
- ] }) : /* @__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." }),
947
- canEdit && sound.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("details", { children: [
948
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("summary", { className: "cursor-pointer text-muted-foreground", children: "Transcripts" }),
949
- /* @__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: [
950
- /* @__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: [
951
1038
  a.name ?? a.id,
952
1039
  " ",
953
1040
  a.transcript ? `\xB7 ${a.transcript.length} words` : ""
954
1041
  ] }),
955
- /* @__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]) })
956
1043
  ] }, a.id)) })
957
1044
  ] }),
958
- 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 })
959
1046
  ] });
960
1047
  }
961
1048
 
962
1049
  // src/video-editor/VideoEditor.tsx
963
- var import_react6 = require("react");
964
- var import_jsx_runtime7 = require("react/jsx-runtime");
965
- 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 = "" }) {
966
1053
  const { controller, state } = useVideoEditor(given ?? options ?? {});
967
- (0, import_react6.useEffect)(() => {
1054
+ (0, import_react7.useEffect)(() => {
968
1055
  if (editable) return;
969
1056
  const was = controller.getState().readOnly;
970
1057
  controller.setReadOnly(true);
971
1058
  return () => controller.setReadOnly(was);
972
1059
  }, [controller, editable]);
973
1060
  const { issues } = state;
974
- 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)) });
975
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: `flex flex-col gap-3 ${className}`, "data-video-editor": "", children: [
976
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: `grid gap-3 ${editable ? "lg:grid-cols-[16rem_minmax(0,1fr)_18rem]" : ""}`, children: [
977
- editable && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(MediaBin, { controller, className: "min-w-0" }),
978
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex min-w-0 flex-col gap-2", children: [
979
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(VideoPreview, { controller, resolution, className: "rounded-md border border-border" }),
980
- /* @__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 })
981
1069
  ] }),
982
- 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" })
983
1071
  ] }),
984
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(VideoTimeline, { controller, editable, pxPerSecond, onPxPerSecondChange }),
985
- 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 }),
986
1074
  issueList
987
1075
  ] });
988
1076
  }
@@ -993,6 +1081,7 @@ function VideoEditor({ controller: given, options, editable = true, pxPerSecond,
993
1081
  TranscriptPanel,
994
1082
  VideoEditor,
995
1083
  VideoPreview,
1084
+ VideoScopes,
996
1085
  VideoTimeline,
997
1086
  VideoTransport,
998
1087
  useVideoEditor,