@pixerate/editor-react 0.77.0 → 0.78.1

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.
@@ -34,6 +34,7 @@ __export(video_editor_exports, {
34
34
  MediaBin: () => MediaBin,
35
35
  TranscriptPanel: () => TranscriptPanel,
36
36
  VideoEditor: () => VideoEditor,
37
+ VideoEffectsPanel: () => VideoEffectsPanel,
37
38
  VideoPreview: () => VideoPreview,
38
39
  VideoScopes: () => VideoScopes,
39
40
  VideoTimeline: () => VideoTimeline,
@@ -41,7 +42,8 @@ __export(video_editor_exports, {
41
42
  useVideoEditor: () => useVideoEditor,
42
43
  useVideoEditorController: () => useVideoEditorController,
43
44
  useVideoEditorState: () => useVideoEditorState,
44
- useVideoEditorTools: () => useVideoEditorTools
45
+ useVideoEditorTools: () => useVideoEditorTools,
46
+ useVideoEffectsTools: () => useVideoEffectsTools
45
47
  });
46
48
  module.exports = __toCommonJS(video_editor_exports);
47
49
 
@@ -91,6 +93,13 @@ function useVideoEditorTools(registry, controller, options = {}) {
91
93
  return registry.register((0, import_video_editor.createVideoEditorToolProvider)(controller, { id, namespace, readOnly, rasterize, framePixels, framePixelsTimeout }));
92
94
  }, [registry, controller, id, namespace, readOnly, rasterize, framePixels, framePixelsTimeout]);
93
95
  }
96
+ function useVideoEffectsTools(registry, controller, options = {}) {
97
+ const { id, namespace, readOnly } = options;
98
+ (0, import_react.useEffect)(() => {
99
+ if (!registry) return;
100
+ return registry.register((0, import_video_editor.createVideoEffectsToolProvider)(controller, { id, namespace, readOnly }));
101
+ }, [registry, controller, id, namespace, readOnly]);
102
+ }
94
103
 
95
104
  // src/video-editor/VideoPreview.tsx
96
105
  var import_react2 = require("react");
@@ -830,6 +839,7 @@ function ClipInspector({ controller, className = "" }) {
830
839
  ] }) }),
831
840
  /* @__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") }) })
832
841
  ] }),
842
+ clip.type === "effect" && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Effect", name: "effect", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "truncate text-xs", title: "Change its controls in the Effects panel", children: clip.name ?? clip.effect }) }),
833
843
  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") }) }),
834
844
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Row, { label: "Transition in", name: "transition", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
835
845
  "select",
@@ -840,14 +850,15 @@ function ClipInspector({ controller, className = "" }) {
840
850
  "aria-label": "Transition",
841
851
  onChange: (e) => {
842
852
  const type = e.target.value;
853
+ const { effect: _effect, ...rest } = transition ?? { duration: DEFAULT_TRANSITION };
843
854
  controller.apply(
844
- { op: "set_transition", clip: clip.id, transition: type ? { ...transition, type, duration: transition?.duration ?? DEFAULT_TRANSITION } : null },
855
+ { op: "set_transition", clip: clip.id, transition: type ? { ...rest, type, duration: transition?.duration ?? DEFAULT_TRANSITION } : null },
845
856
  type ? "Set transition" : "Remove transition"
846
857
  );
847
858
  },
848
859
  children: [
849
860
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "", children: "None" }),
850
- 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))
861
+ import_video_editor6.VIDEO_TRANSITION_TYPES.filter((t) => t !== "effect" || transition?.type === "effect").map((t) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: t, children: t === "effect" ? `Effect: ${transition?.effect?.name ?? transition?.effect?.effect}` : import_video_editor6.VIDEO_TRANSITION_LABELS[t] }, t))
851
862
  ]
852
863
  }
853
864
  ) }),
@@ -1148,30 +1159,732 @@ function TranscriptPanel({ controller, track, editable = true, className = "" })
1148
1159
  }
1149
1160
 
1150
1161
  // src/video-editor/VideoEditor.tsx
1162
+ var import_react14 = require("react");
1163
+
1164
+ // src/effects/useEffects.ts
1151
1165
  var import_react8 = require("react");
1166
+ function useEffects(host, provider) {
1167
+ (0, import_react8.useEffect)(() => {
1168
+ if (!host || !provider) return;
1169
+ return host.useEffects(provider);
1170
+ }, [host, provider]);
1171
+ }
1172
+ function useEffectDescription(provider, effect) {
1173
+ const [state, setState] = (0, import_react8.useState)({ key: "", description: null, error: null });
1174
+ const key = provider && effect ? `${provider.id}:${effect}` : "";
1175
+ (0, import_react8.useEffect)(() => {
1176
+ if (!provider || !effect) return;
1177
+ let live = true;
1178
+ Promise.resolve().then(() => provider.describe(effect)).then(
1179
+ (description) => live && setState({ key, description, error: null }),
1180
+ (e) => live && setState({ key, description: null, error: e instanceof Error ? e.message : String(e) })
1181
+ );
1182
+ return () => {
1183
+ live = false;
1184
+ };
1185
+ }, [provider, effect, key]);
1186
+ return state.key === key ? { description: state.description, error: state.error } : { description: null, error: null };
1187
+ }
1188
+
1189
+ // src/effects/EffectThumbnail.tsx
1190
+ var import_react9 = require("react");
1191
+ var import_effects = require("@pixerate/editor/effects");
1152
1192
  var import_jsx_runtime8 = require("react/jsx-runtime");
1153
- function VideoEditor({ controller: given, options, editable = true, pxPerSecond, onPxPerSecondChange, resolution, showIssues = true, showTranscript = true, showScopes = false, className = "" }) {
1193
+ function EffectThumbnail({ provider, effect, width = 96, height = 54, className = "" }) {
1194
+ const ref = (0, import_react9.useRef)(null);
1195
+ const [failed, setFailed] = (0, import_react9.useState)(effect.kind === "motion");
1196
+ (0, import_react9.useEffect)(() => {
1197
+ if (effect.kind === "motion") return;
1198
+ let live = true;
1199
+ (0, import_effects.renderEffectThumbnail)(provider, effect, { width, height }).then(
1200
+ (pixels) => {
1201
+ const ctx = live ? ref.current?.getContext("2d") : null;
1202
+ if (!ctx || typeof ImageData === "undefined") return;
1203
+ ctx.putImageData(new ImageData(new Uint8ClampedArray(pixels.data), pixels.width, pixels.height), 0, 0);
1204
+ },
1205
+ () => live && setFailed(true)
1206
+ );
1207
+ return () => {
1208
+ live = false;
1209
+ };
1210
+ }, [provider, effect, width, height]);
1211
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1212
+ "div",
1213
+ {
1214
+ className: `relative overflow-hidden rounded border border-border bg-[repeating-conic-gradient(#8884_0_25%,transparent_0_50%)] bg-[length:12px_12px] ${className}`,
1215
+ style: { aspectRatio: `${width} / ${height}` },
1216
+ "data-effect-thumbnail": effect.id,
1217
+ children: failed ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "absolute inset-0 flex items-center justify-center text-[10px] text-muted-foreground", children: effect.kind === "motion" ? "Motion" : "No preview" }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("canvas", { ref, width, height, className: "h-full w-full", "aria-hidden": "true" })
1218
+ }
1219
+ );
1220
+ }
1221
+
1222
+ // src/effects/EffectsBrowser.tsx
1223
+ var import_react10 = require("react");
1224
+ var import_effects2 = require("@pixerate/editor/effects");
1225
+ var import_jsx_runtime9 = require("react/jsx-runtime");
1226
+ function EffectsBrowser({ provider, uses, onPick, thumbnails = true, title = "Effects", className = "" }) {
1227
+ const [all, setAll] = (0, import_react10.useState)([]);
1228
+ const [query, setQuery] = (0, import_react10.useState)("");
1229
+ const [category, setCategory] = (0, import_react10.useState)("");
1230
+ const [use, setUse] = (0, import_react10.useState)("");
1231
+ const [error, setError] = (0, import_react10.useState)(null);
1232
+ (0, import_react10.useEffect)(() => {
1233
+ if (!provider) return setAll([]);
1234
+ let live = true;
1235
+ Promise.resolve().then(() => provider.list()).then(
1236
+ (list) => live && (setAll(list), setError(null)),
1237
+ (e) => live && setError(e instanceof Error ? e.message : String(e))
1238
+ );
1239
+ return () => {
1240
+ live = false;
1241
+ };
1242
+ }, [provider]);
1243
+ const [found, setFound] = (0, import_react10.useState)([]);
1244
+ (0, import_react10.useEffect)(() => {
1245
+ if (!provider) return setFound([]);
1246
+ let live = true;
1247
+ Promise.resolve().then(() => provider.search(query, { ...category ? { category } : {}, ...use ? { use } : {} })).then((list) => live && setFound(list), () => live && setFound([]));
1248
+ return () => {
1249
+ live = false;
1250
+ };
1251
+ }, [provider, query, category, use]);
1252
+ const allowed = (e) => (uses ? e.kind !== "motion" || uses.includes("motion") : e.kind !== "motion") && (!uses || e.uses.some((u) => uses.includes(u)));
1253
+ const categories = (0, import_react10.useMemo)(() => [...new Set(all.filter(allowed).map((e) => e.category))], [all, uses]);
1254
+ const shown = found.filter(allowed);
1255
+ if (!provider)
1256
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: `rounded-md border border-dashed border-border p-3 text-xs text-muted-foreground ${className}`, "data-effects-browser": "", "data-effects-unavailable": "", children: "No effects provider is registered, so effects cannot be added. Effects already in the document are kept." });
1257
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("section", { className: `flex flex-col gap-2 ${className}`, "data-effects-browser": "", "aria-label": "Effects browser", children: [
1258
+ title && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("h3", { className: "text-xs font-semibold", children: title }),
1259
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex flex-wrap gap-1", children: [
1260
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1261
+ "input",
1262
+ {
1263
+ type: "search",
1264
+ className: "min-w-0 flex-1 rounded border border-border bg-transparent px-1.5 py-0.5 text-xs",
1265
+ placeholder: "Search effects",
1266
+ "aria-label": "Search effects",
1267
+ value: query,
1268
+ onChange: (e) => setQuery(e.currentTarget.value)
1269
+ }
1270
+ ),
1271
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("select", { className: "rounded border border-border bg-transparent px-1 py-0.5 text-xs", "aria-label": "Category", value: category, onChange: (e) => setCategory(e.currentTarget.value), children: [
1272
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("option", { value: "", children: "All categories" }),
1273
+ categories.map((c) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("option", { value: c, children: c }, c))
1274
+ ] }),
1275
+ uses && uses.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("select", { className: "rounded border border-border bg-transparent px-1 py-0.5 text-xs", "aria-label": "Use", value: use, onChange: (e) => setUse(e.currentTarget.value), children: [
1276
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("option", { value: "", children: "Any use" }),
1277
+ uses.map((u) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("option", { value: u, children: import_effects2.EFFECT_USE_LABELS[u] }, u))
1278
+ ] })
1279
+ ] }),
1280
+ error && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { role: "alert", className: "text-[11px] text-destructive", children: error }),
1281
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("ul", { className: "grid max-h-96 grid-cols-[repeat(auto-fill,minmax(6.5rem,1fr))] gap-1.5 overflow-y-auto", role: "list", children: shown.map((e) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
1282
+ "button",
1283
+ {
1284
+ type: "button",
1285
+ className: "flex w-full flex-col gap-1 rounded border border-transparent p-1 text-left hover:border-border focus-visible:border-border",
1286
+ title: e.description ?? e.name,
1287
+ "data-effect-id": e.id,
1288
+ onClick: () => onPick(e),
1289
+ children: [
1290
+ thumbnails && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(EffectThumbnail, { provider, effect: e }),
1291
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "truncate text-[11px] font-medium", children: e.name }),
1292
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "truncate text-[10px] text-muted-foreground", children: e.uses.map((u) => import_effects2.EFFECT_USE_LABELS[u]).join(" \xB7 ") })
1293
+ ]
1294
+ }
1295
+ ) }, e.id)) }),
1296
+ !shown.length && !error && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: "text-[11px] text-muted-foreground", children: "No effects match." })
1297
+ ] });
1298
+ }
1299
+
1300
+ // src/effects/EffectParameters.tsx
1301
+ var import_react12 = __toESM(require("react"), 1);
1302
+ var import_effects3 = require("@pixerate/editor/effects");
1303
+ var import_parameters2 = require("@pixerate/editor/parameters");
1304
+
1305
+ // src/parameters/fields.tsx
1306
+ var import_react11 = require("react");
1307
+ var import_parameters = require("@pixerate/editor/parameters");
1308
+ var import_jsx_runtime10 = require("react/jsx-runtime");
1309
+ var parameterInputClass = "h-7 w-full min-w-0 rounded-md border border-input bg-background px-2 text-xs text-foreground tabular-nums focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary disabled:cursor-not-allowed disabled:opacity-50";
1310
+ var parameterButtonClass = "inline-flex h-7 items-center justify-center gap-1 rounded-md border border-input bg-background px-2 text-xs text-foreground hover:bg-muted focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50";
1311
+ var rangeClass = "h-4 w-full min-w-0 flex-1 cursor-pointer accent-primary disabled:cursor-not-allowed disabled:opacity-50";
1312
+ function ParameterCommitInput({ value, onCommit, multiline, className, onKeyDown, ...rest }) {
1313
+ const commit = (el) => {
1314
+ if (el.value !== value) onCommit(el.value, el);
1315
+ };
1316
+ const keyDown = (e) => {
1317
+ onKeyDown?.(e);
1318
+ if (e.defaultPrevented) return;
1319
+ if (e.key === "Enter" && (!multiline || e.metaKey || e.ctrlKey)) {
1320
+ e.preventDefault();
1321
+ commit(e.currentTarget);
1322
+ }
1323
+ if (e.key === "Escape") e.currentTarget.value = value;
1324
+ };
1325
+ const props = {
1326
+ defaultValue: value,
1327
+ className: className ?? parameterInputClass,
1328
+ onBlur: (e) => commit(e.currentTarget),
1329
+ onKeyDown: keyDown,
1330
+ ...rest
1331
+ };
1332
+ return multiline ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("textarea", { ...props, className: `${props.className} h-auto min-h-[3.5rem] py-1` }, value) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("input", { type: "text", ...props }, value);
1333
+ }
1334
+ var plainNumber = (field2, value) => (0, import_parameters.formatParameterNumber)({ ...field2, unit: void 0 }, value);
1335
+ function ParameterNumberInput({
1336
+ field: field2,
1337
+ value,
1338
+ onChange,
1339
+ disabled,
1340
+ id,
1341
+ "aria-label": ariaLabel
1342
+ }) {
1343
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "relative min-w-0 flex-1", children: [
1344
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1345
+ ParameterCommitInput,
1346
+ {
1347
+ id,
1348
+ inputMode: "decimal",
1349
+ "aria-label": ariaLabel ?? (0, import_parameters.parameterLabel)(field2),
1350
+ value: plainNumber(field2, value),
1351
+ disabled: disabled || field2.readOnly,
1352
+ className: `${parameterInputClass} ${field2.unit ? "pr-6" : ""}`,
1353
+ onCommit: (text, input) => {
1354
+ const n = (0, import_parameters.parseParameterNumberInput)(field2, text);
1355
+ if (n === null || n === value) input.value = plainNumber(field2, value);
1356
+ else onChange(n);
1357
+ },
1358
+ onKeyDown: (e) => {
1359
+ if (e.key !== "ArrowUp" && e.key !== "ArrowDown") return;
1360
+ e.preventDefault();
1361
+ const step = (0, import_parameters.parameterStep)(field2) * (e.shiftKey ? 10 : 1);
1362
+ onChange((0, import_parameters.clampParameterNumber)(field2, value + (e.key === "ArrowUp" ? step : -step)));
1363
+ }
1364
+ }
1365
+ ),
1366
+ field2.unit && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 text-[10px] text-muted-foreground", children: field2.unit })
1367
+ ] });
1368
+ }
1369
+ function ParameterSlider(props) {
1370
+ const { field: field2, value, onChange, onGestureStart, onGestureEnd, disabled } = props;
1371
+ const dragging = (0, import_react11.useRef)(false);
1372
+ const end = () => {
1373
+ if (!dragging.current) return;
1374
+ dragging.current = false;
1375
+ onGestureEnd?.();
1376
+ };
1377
+ if (!(0, import_parameters.isSliderField)(field2)) return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ParameterNumberInput, { ...props });
1378
+ const step = (0, import_parameters.parameterStep)(field2);
1379
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex items-center gap-2", children: [
1380
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1381
+ "input",
1382
+ {
1383
+ type: "range",
1384
+ "aria-label": (0, import_parameters.parameterLabel)(field2),
1385
+ "aria-valuetext": (0, import_parameters.formatParameterNumber)(field2, value),
1386
+ min: field2.min,
1387
+ max: field2.max,
1388
+ step,
1389
+ value,
1390
+ disabled: disabled || field2.readOnly,
1391
+ className: rangeClass,
1392
+ onPointerDown: () => {
1393
+ if (dragging.current) return;
1394
+ dragging.current = true;
1395
+ onGestureStart?.();
1396
+ },
1397
+ onPointerUp: end,
1398
+ onPointerCancel: end,
1399
+ onBlur: end,
1400
+ onChange: (e) => onChange((0, import_parameters.clampParameterNumber)(field2, Number(e.target.value)))
1401
+ }
1402
+ ),
1403
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "w-20 shrink-0", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ParameterNumberInput, { ...props, id: props.id, "aria-label": `${(0, import_parameters.parameterLabel)(field2)} value` }) })
1404
+ ] });
1405
+ }
1406
+ function ParameterToggle({ field: field2, value, onChange, disabled, id }) {
1407
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1408
+ "button",
1409
+ {
1410
+ type: "button",
1411
+ role: "switch",
1412
+ id,
1413
+ "aria-checked": value,
1414
+ "aria-label": (0, import_parameters.parameterLabel)(field2),
1415
+ disabled: disabled || field2.readOnly,
1416
+ onClick: () => onChange(!value),
1417
+ className: `relative inline-flex h-5 w-9 shrink-0 items-center rounded-full border border-transparent transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary disabled:opacity-50 ${value ? "bg-primary" : "bg-muted"}`,
1418
+ children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1419
+ "span",
1420
+ {
1421
+ className: `pointer-events-none block h-4 w-4 rounded-full bg-background shadow transition-transform ${value ? "translate-x-4" : "translate-x-0.5"}`
1422
+ }
1423
+ )
1424
+ }
1425
+ );
1426
+ }
1427
+ function ParameterSelect({ field: field2, value, onChange, disabled, id }) {
1428
+ const options = (0, import_parameters.getChoiceOptions)(field2);
1429
+ const off = disabled || field2.readOnly;
1430
+ if (field2.control === "segmented") {
1431
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { role: "radiogroup", "aria-label": (0, import_parameters.parameterLabel)(field2), className: "inline-flex flex-wrap rounded-md border border-input bg-muted p-0.5", children: options.map((o) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1432
+ "button",
1433
+ {
1434
+ type: "button",
1435
+ role: "radio",
1436
+ "aria-checked": o.value === value,
1437
+ title: o.description,
1438
+ disabled: off,
1439
+ onClick: () => onChange(o.value),
1440
+ className: `rounded px-2 py-0.5 text-xs disabled:opacity-50 ${o.value === value ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"}`,
1441
+ children: (0, import_parameters.choiceOptionLabel)(o)
1442
+ },
1443
+ String(o.value)
1444
+ )) });
1445
+ }
1446
+ const index = options.findIndex((o) => o.value === value);
1447
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1448
+ "select",
1449
+ {
1450
+ id,
1451
+ "aria-label": (0, import_parameters.parameterLabel)(field2),
1452
+ value: String(index),
1453
+ disabled: off,
1454
+ onChange: (e) => {
1455
+ const option = options[Number(e.target.value)];
1456
+ if (option) onChange(option.value);
1457
+ },
1458
+ className: parameterInputClass,
1459
+ children: options.map((o, i) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("option", { value: String(i), title: o.description, children: (0, import_parameters.choiceOptionLabel)(o) }, String(o.value)))
1460
+ }
1461
+ );
1462
+ }
1463
+ function ParameterColor({ field: field2, value, onChange, onGestureStart, onGestureEnd, disabled, id }) {
1464
+ const picking = (0, import_react11.useRef)(false);
1465
+ const alpha = !!field2.alpha;
1466
+ const hex = (0, import_parameters.normalizeHexColor)(value, true) ?? "#000000ff";
1467
+ const rgb = hex.slice(0, 7);
1468
+ const a = hex.slice(7);
1469
+ const off = disabled || field2.readOnly;
1470
+ const start = () => {
1471
+ if (picking.current) return;
1472
+ picking.current = true;
1473
+ onGestureStart?.();
1474
+ };
1475
+ const end = () => {
1476
+ if (!picking.current) return;
1477
+ picking.current = false;
1478
+ onGestureEnd?.();
1479
+ };
1480
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "space-y-1.5", children: [
1481
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex items-center gap-2", children: [
1482
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { className: "relative h-7 w-9 shrink-0 overflow-hidden rounded-md border border-input bg-[repeating-conic-gradient(#d4d4d8_0_25%,#fff_0_50%)] bg-[length:8px_8px]", children: [
1483
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "absolute inset-0", style: { background: alpha ? hex : rgb } }),
1484
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1485
+ "input",
1486
+ {
1487
+ type: "color",
1488
+ "aria-label": (0, import_parameters.parameterLabel)(field2),
1489
+ value: rgb,
1490
+ disabled: off,
1491
+ className: "absolute inset-0 h-full w-full cursor-pointer opacity-0 disabled:cursor-not-allowed",
1492
+ onChange: (e) => {
1493
+ start();
1494
+ onChange(alpha ? `${e.target.value}${a}` : e.target.value);
1495
+ },
1496
+ onBlur: end
1497
+ }
1498
+ )
1499
+ ] }),
1500
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1501
+ ParameterCommitInput,
1502
+ {
1503
+ id,
1504
+ "aria-label": `${(0, import_parameters.parameterLabel)(field2)} hex`,
1505
+ value: alpha ? hex : rgb,
1506
+ disabled: off,
1507
+ spellCheck: false,
1508
+ className: `${parameterInputClass} font-mono`,
1509
+ onCommit: (text) => {
1510
+ const next = (0, import_parameters.normalizeHexColor)(text, alpha);
1511
+ if (next) onChange(next);
1512
+ }
1513
+ }
1514
+ )
1515
+ ] }),
1516
+ alpha && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1517
+ "input",
1518
+ {
1519
+ type: "range",
1520
+ "aria-label": `${(0, import_parameters.parameterLabel)(field2)} opacity`,
1521
+ min: 0,
1522
+ max: 255,
1523
+ step: 1,
1524
+ value: parseInt(a, 16),
1525
+ disabled: off,
1526
+ className: rangeClass,
1527
+ onPointerDown: start,
1528
+ onPointerUp: end,
1529
+ onPointerCancel: end,
1530
+ onBlur: end,
1531
+ onChange: (e) => onChange(`${rgb}${Number(e.target.value).toString(16).padStart(2, "0")}`)
1532
+ }
1533
+ ),
1534
+ field2.swatches && field2.swatches.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "flex flex-wrap gap-1", children: field2.swatches.map((s) => {
1535
+ const swatch = (0, import_parameters.normalizeHexColor)(s, alpha);
1536
+ if (!swatch) return null;
1537
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1538
+ "button",
1539
+ {
1540
+ type: "button",
1541
+ title: swatch,
1542
+ "aria-label": `${(0, import_parameters.parameterLabel)(field2)} ${swatch}`,
1543
+ "aria-pressed": swatch === (alpha ? hex : rgb),
1544
+ disabled: off,
1545
+ onClick: () => onChange(swatch),
1546
+ className: "h-5 w-5 rounded border border-input aria-pressed:ring-1 aria-pressed:ring-primary",
1547
+ style: { background: swatch }
1548
+ },
1549
+ s
1550
+ );
1551
+ }) })
1552
+ ] });
1553
+ }
1554
+ function ParameterVector({ field: field2, value, onChange, disabled, id }) {
1555
+ const size = (0, import_parameters.vectorSize)(field2);
1556
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "grid gap-1.5", style: { gridTemplateColumns: `repeat(${size}, minmax(0, 1fr))` }, children: Array.from({ length: size }, (_, i) => {
1557
+ const component = (0, import_parameters.vectorComponentField)(field2, i);
1558
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("label", { className: "flex min-w-0 items-center gap-1", children: [
1559
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "w-3 shrink-0 text-[10px] uppercase text-muted-foreground", children: component.label }),
1560
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1561
+ ParameterNumberInput,
1562
+ {
1563
+ field: component,
1564
+ value: value[i] ?? 0,
1565
+ id: i === 0 ? id : void 0,
1566
+ "aria-label": `${(0, import_parameters.parameterLabel)(field2)} ${component.label}`,
1567
+ disabled: disabled || field2.readOnly,
1568
+ onChange: (n) => onChange(Array.from({ length: size }, (_2, j) => j === i ? n : value[j] ?? 0))
1569
+ }
1570
+ )
1571
+ ] }, i);
1572
+ }) });
1573
+ }
1574
+ function ParameterImage({ field: field2, value, onChange, disabled, id, resolveFile }) {
1575
+ const off = disabled || field2.readOnly;
1576
+ const name = (0, import_parameters.parameterLabel)(field2);
1577
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "space-y-1.5", children: [
1578
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex items-center gap-2", children: [
1579
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "flex h-12 w-16 shrink-0 items-center justify-center overflow-hidden rounded-md border border-input bg-muted", children: value ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("img", { src: value, alt: name, className: "h-full w-full object-cover" }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "text-[10px] text-muted-foreground", children: "None" }) }),
1580
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [
1581
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1582
+ ParameterCommitInput,
1583
+ {
1584
+ id,
1585
+ "aria-label": `${name} URL`,
1586
+ placeholder: "Image URL",
1587
+ value: value.startsWith("data:") ? "" : value,
1588
+ disabled: off,
1589
+ onCommit: (text) => onChange(text.trim())
1590
+ }
1591
+ ),
1592
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex gap-1", children: [
1593
+ resolveFile && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("label", { className: `${parameterButtonClass} cursor-pointer ${off ? "pointer-events-none opacity-50" : ""}`, children: [
1594
+ "Choose file\u2026",
1595
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1596
+ "input",
1597
+ {
1598
+ type: "file",
1599
+ className: "sr-only",
1600
+ accept: field2.accept ?? "image/*",
1601
+ disabled: off,
1602
+ "aria-label": `${name} file`,
1603
+ onChange: async (e) => {
1604
+ const file = e.target.files?.[0];
1605
+ e.target.value = "";
1606
+ if (file) onChange(await resolveFile(file));
1607
+ }
1608
+ }
1609
+ )
1610
+ ] }),
1611
+ value && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "button", className: parameterButtonClass, disabled: off, onClick: () => onChange(""), "aria-label": `Clear ${name}`, children: "Clear" })
1612
+ ] })
1613
+ ] })
1614
+ ] }),
1615
+ field2.options && field2.options.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "flex flex-wrap gap-1", children: field2.options.map((o) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1616
+ "button",
1617
+ {
1618
+ type: "button",
1619
+ title: o.label ?? o.url,
1620
+ "aria-label": o.label ?? o.url,
1621
+ "aria-pressed": o.url === value,
1622
+ disabled: off,
1623
+ onClick: () => onChange(o.url),
1624
+ className: "h-9 w-12 overflow-hidden rounded border border-input aria-pressed:ring-2 aria-pressed:ring-primary",
1625
+ children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("img", { src: o.url, alt: "", className: "h-full w-full object-cover" })
1626
+ },
1627
+ o.url
1628
+ )) })
1629
+ ] });
1630
+ }
1631
+ function ParameterText({ field: field2, value, onChange, disabled, id }) {
1632
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1633
+ ParameterCommitInput,
1634
+ {
1635
+ id,
1636
+ "aria-label": (0, import_parameters.parameterLabel)(field2),
1637
+ multiline: field2.multiline,
1638
+ placeholder: field2.placeholder,
1639
+ maxLength: field2.maxLength,
1640
+ value,
1641
+ disabled: disabled || field2.readOnly,
1642
+ onCommit: onChange
1643
+ }
1644
+ );
1645
+ }
1646
+ function ParameterFieldControl({ field: field2, value, onChange, resolveFile, ...rest }) {
1647
+ const set = onChange;
1648
+ switch (field2.type) {
1649
+ case "number":
1650
+ case "int":
1651
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ParameterSlider, { field: field2, value, onChange: set, ...rest });
1652
+ case "boolean":
1653
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ParameterToggle, { field: field2, value, onChange: set, ...rest });
1654
+ case "choice":
1655
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ParameterSelect, { field: field2, value, onChange: set, ...rest });
1656
+ case "color":
1657
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ParameterColor, { field: field2, value, onChange: set, ...rest });
1658
+ case "vector":
1659
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ParameterVector, { field: field2, value, onChange: set, ...rest });
1660
+ case "image":
1661
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ParameterImage, { field: field2, value, onChange: set, resolveFile, ...rest });
1662
+ case "text":
1663
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ParameterText, { field: field2, value, onChange: set, ...rest });
1664
+ }
1665
+ }
1666
+
1667
+ // src/effects/EffectParameters.tsx
1668
+ var import_jsx_runtime11 = require("react/jsx-runtime");
1669
+ function EffectParameters({ description, values, onChange, onReset, keyframed, onPreset, disabled = false, className = "" }) {
1670
+ const fields = (0, import_react12.useMemo)(() => (0, import_effects3.effectParameterFields)(description), [description]);
1671
+ const idBase = (0, import_react12.useId)();
1672
+ const gesture = import_react12.default.useRef(null);
1673
+ const count = import_react12.default.useRef(0);
1674
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: `space-y-2 ${className}`, "data-effect-parameters": description.id, children: [
1675
+ onPreset && description.presets.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1676
+ "select",
1677
+ {
1678
+ className: "w-full rounded border border-border bg-transparent px-1 py-0.5 text-xs",
1679
+ "aria-label": `${description.name} presets`,
1680
+ value: "",
1681
+ disabled,
1682
+ onChange: (e) => {
1683
+ const preset = description.presets.find((p) => p.name === e.currentTarget.value);
1684
+ if (preset) onPreset({ ...description.defaults, ...preset.values });
1685
+ },
1686
+ children: [
1687
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("option", { value: "", children: "Presets\u2026" }),
1688
+ description.presets.map((p) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("option", { value: p.name, children: p.name }, p.name))
1689
+ ]
1690
+ }
1691
+ ),
1692
+ fields.map((field2) => {
1693
+ const label = (0, import_parameters2.parameterLabel)(field2);
1694
+ const inputId = `${idBase}-${field2.id}`;
1695
+ const keyed = keyframed?.has(field2.id) ?? false;
1696
+ const value = (0, import_effects3.effectFieldValue)(field2, values[field2.id]);
1697
+ const changed = !keyed && !(0, import_parameters2.parameterValuesEqual)(value, field2.default);
1698
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "space-y-1", "data-effect-control": field2.id, children: [
1699
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex items-center gap-1.5", children: [
1700
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("label", { htmlFor: inputId, className: "min-w-0 flex-1 truncate text-xs font-medium", title: field2.description, children: label }),
1701
+ keyed && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "text-[10px] text-muted-foreground", children: "keyframed" }),
1702
+ onReset && changed && !disabled && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("button", { type: "button", className: "rounded px-1 text-[11px] text-muted-foreground hover:text-foreground", "aria-label": `Reset ${label}`, onClick: () => onReset(field2.id), children: "\u21BA" })
1703
+ ] }),
1704
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1705
+ ParameterFieldControl,
1706
+ {
1707
+ field: field2,
1708
+ value,
1709
+ id: inputId,
1710
+ disabled: disabled || keyed,
1711
+ onChange: (v) => onChange(field2.id, v, gesture.current),
1712
+ onGestureStart: () => gesture.current = `${idBase}:${field2.id}:${++count.current}`,
1713
+ onGestureEnd: () => gesture.current = null
1714
+ }
1715
+ )
1716
+ ] }, field2.id);
1717
+ }),
1718
+ !fields.length && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "text-[11px] text-muted-foreground", children: "No controls." })
1719
+ ] });
1720
+ }
1721
+
1722
+ // src/video-editor/VideoEffectsPanel.tsx
1723
+ var import_react13 = require("react");
1724
+ var import_video_editor9 = require("@pixerate/editor/video-editor");
1725
+ var import_jsx_runtime12 = require("react/jsx-runtime");
1726
+ var btn5 = "rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40";
1727
+ var keyedOf = (values) => new Set(Object.entries(values ?? {}).filter(([, v]) => (0, import_video_editor9.isVideoKeyframes)(v)).map(([k]) => k));
1728
+ function EffectControls({
1729
+ provider,
1730
+ effect,
1731
+ name,
1732
+ values,
1733
+ disabled,
1734
+ onChange,
1735
+ onPreset
1736
+ }) {
1737
+ const { description, error } = useEffectDescription(provider, effect);
1738
+ if (!provider) return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("p", { className: "text-[11px] text-muted-foreground", children: [
1739
+ 'No effects provider: "',
1740
+ name ?? effect,
1741
+ '" is kept and shown as a placeholder.'
1742
+ ] });
1743
+ if (error) return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "text-[11px] text-amber-500", children: error });
1744
+ if (!description) return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "text-[11px] text-muted-foreground", children: "Loading\u2026" });
1745
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(EffectParameters, { description, values: values ?? {}, keyframed: keyedOf(values), disabled, onChange, onReset: (k) => onChange(k, description.defaults[k], null), onPreset });
1746
+ }
1747
+ function VideoEffectsPanel({ controller, className = "" }) {
1748
+ const state = useVideoEditorState(controller);
1749
+ const provider = state.effectsProvider;
1750
+ const [browse, setBrowse] = (0, import_react13.useState)(null);
1751
+ const [message, setMessage] = (0, import_react13.useState)(null);
1752
+ const [open, setOpen] = (0, import_react13.useState)(null);
1753
+ const selected = state.selection[0];
1754
+ const at = selected ? (0, import_video_editor9.findVideoClip)(state.project, selected) : null;
1755
+ const clip = at?.clip ?? null;
1756
+ const disabled = state.readOnly;
1757
+ const report = (r) => void Promise.resolve(r).then((x) => setMessage(x.ok ? null : x.error));
1758
+ const addEffectClip = (effect) => {
1759
+ const track = at && at.track.kind === "video" ? at.track.id : state.project.tracks.find((t) => t.kind === "video")?.id;
1760
+ if (!track) return setMessage("Add a video track first.");
1761
+ report(controller.addEffectClip(track, effect, { start: Math.round(state.time), mode: "overwrite" }).then((r) => (r.ok && r.clip && controller.select([r.clip]), r)));
1762
+ };
1763
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("section", { className: `flex flex-col gap-2 rounded-md border border-border p-2 text-xs ${className}`, "aria-label": "Effects", "data-video-effects": "", children: [
1764
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex items-center gap-1", children: [
1765
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("h3", { className: "flex-1 font-semibold", children: "Effects" }),
1766
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("button", { type: "button", className: btn5, disabled: disabled || !provider, onClick: () => setBrowse(browse === "generator" ? null : "generator"), "aria-pressed": browse === "generator", children: "Add effect clip" })
1767
+ ] }),
1768
+ !provider && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "text-[11px] text-muted-foreground", children: "No effects provider is registered. Effects in the project are kept and drawn as placeholders." }),
1769
+ browse === "generator" && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(EffectsBrowser, { provider, uses: ["generator", "title", "overlay"], title: "Generators and titles", onPick: (e) => (setBrowse(null), addEffectClip(e.id)) }),
1770
+ !clip && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "text-[11px] text-muted-foreground", children: "Select a clip to see its effects." }),
1771
+ clip && /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_jsx_runtime12.Fragment, { children: [
1772
+ clip.type === "effect" && /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "space-y-1", "data-effect-part": "generator", children: [
1773
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "font-medium", children: clip.name ?? clip.effect }),
1774
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1775
+ EffectControls,
1776
+ {
1777
+ provider,
1778
+ effect: clip.effect,
1779
+ name: clip.name,
1780
+ values: clip.values,
1781
+ disabled,
1782
+ onChange: (k, v, gesture) => report(controller.updateEffectValues(clip.id, "generator", { [k]: v }, { merge: gesture ?? void 0 })),
1783
+ onPreset: (values) => report(controller.updateEffectValues(clip.id, "generator", values))
1784
+ }
1785
+ )
1786
+ ] }),
1787
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "space-y-1", "data-effect-part": "transition", children: [
1788
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex items-center gap-1", children: [
1789
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "flex-1 font-medium", children: "Transition in" }),
1790
+ clip.transition?.type === "effect" ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("button", { type: "button", className: btn5, disabled, onClick: () => report(controller.apply({ op: "set_transition", clip: clip.id, transition: null }, "Remove transition")), children: "Remove" }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("button", { type: "button", className: btn5, disabled: disabled || !provider, "aria-pressed": browse === "transition", onClick: () => setBrowse(browse === "transition" ? null : "transition"), children: "Effect transition\u2026" })
1791
+ ] }),
1792
+ browse === "transition" && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(EffectsBrowser, { provider, uses: ["transition"], title: "Transitions", onPick: (e) => (setBrowse(null), report(controller.setEffectTransition(clip.id, e.id))) }),
1793
+ clip.transition?.type === "effect" && clip.transition.effect && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1794
+ EffectControls,
1795
+ {
1796
+ provider,
1797
+ effect: clip.transition.effect.effect,
1798
+ name: clip.transition.effect.name,
1799
+ values: clip.transition.effect.values,
1800
+ disabled,
1801
+ onChange: (k, v, gesture) => report(controller.updateEffectValues(clip.id, "transition", { [k]: v }, { merge: gesture ?? void 0 })),
1802
+ onPreset: (values) => report(controller.updateEffectValues(clip.id, "transition", values))
1803
+ }
1804
+ )
1805
+ ] }),
1806
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "space-y-1", "data-effect-part": "stack", children: [
1807
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex items-center gap-1", children: [
1808
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "flex-1 font-medium", children: "On this clip" }),
1809
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("button", { type: "button", className: btn5, disabled: disabled || !provider, "aria-pressed": browse === "clip", onClick: () => setBrowse(browse === "clip" ? null : "clip"), children: "Add effect\u2026" })
1810
+ ] }),
1811
+ browse === "clip" && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(EffectsBrowser, { provider, uses: ["filter", "overlay"], title: "Filters and overlays", onPick: (e) => (setBrowse(null), report(controller.applyClipEffect(clip.id, e.id))) }),
1812
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("ol", { className: "space-y-1", children: (clip.effects ?? []).map((e, i, all) => /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("li", { className: "rounded border border-border p-1", "data-clip-effect": e.id, children: [
1813
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex items-center gap-1", children: [
1814
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("input", { type: "checkbox", checked: e.enabled !== false, disabled, "aria-label": `${e.name ?? e.effect} on`, onChange: (ev) => report(controller.updateClipEffect(clip.id, e.id, { enabled: ev.currentTarget.checked })) }),
1815
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("button", { type: "button", className: "min-w-0 flex-1 truncate text-left", "aria-expanded": open === e.id, onClick: () => setOpen(open === e.id ? null : e.id), children: e.name ?? e.effect }),
1816
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("button", { type: "button", className: btn5, disabled: disabled || i === 0, "aria-label": "Move up", onClick: () => report(controller.updateClipEffect(clip.id, e.id, { index: i - 1 })), children: "\u2191" }),
1817
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("button", { type: "button", className: btn5, disabled: disabled || i === all.length - 1, "aria-label": "Move down", onClick: () => report(controller.updateClipEffect(clip.id, e.id, { index: i + 1 })), children: "\u2193" }),
1818
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("button", { type: "button", className: btn5, disabled, "aria-label": `Remove ${e.name ?? e.effect}`, onClick: () => report(controller.removeClipEffect(clip.id, e.id)), children: "\u2715" })
1819
+ ] }),
1820
+ open === e.id && /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "mt-1 space-y-2", children: [
1821
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("label", { className: "flex items-center gap-1", children: [
1822
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "w-14 text-[11px]", children: "Opacity" }),
1823
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1824
+ "input",
1825
+ {
1826
+ type: "range",
1827
+ min: 0,
1828
+ max: 1,
1829
+ step: 0.01,
1830
+ className: "flex-1",
1831
+ disabled: disabled || (0, import_video_editor9.isVideoKeyframes)(e.opacity),
1832
+ value: typeof e.opacity === "number" ? e.opacity : 1,
1833
+ "aria-label": `${e.name ?? e.effect} opacity`,
1834
+ onChange: (ev) => report(controller.updateClipEffect(clip.id, e.id, { opacity: Number(ev.currentTarget.value) }, { merge: `opacity:${clip.id}:${e.id}` }))
1835
+ }
1836
+ )
1837
+ ] }),
1838
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1839
+ EffectControls,
1840
+ {
1841
+ provider,
1842
+ effect: e.effect,
1843
+ name: e.name,
1844
+ values: e.values,
1845
+ disabled,
1846
+ onChange: (k, v, gesture) => report(controller.updateClipEffect(clip.id, e.id, { values: { [k]: v } }, { merge: gesture ?? void 0 })),
1847
+ onPreset: (values) => report(controller.updateClipEffect(clip.id, e.id, { values }))
1848
+ }
1849
+ )
1850
+ ] })
1851
+ ] }, e.id)) }),
1852
+ !clip.effects?.length && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "text-[11px] text-muted-foreground", children: "No effects on this clip." })
1853
+ ] })
1854
+ ] }),
1855
+ message && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { role: "alert", className: "text-[11px] text-destructive", children: message })
1856
+ ] });
1857
+ }
1858
+
1859
+ // src/video-editor/VideoEditor.tsx
1860
+ var import_jsx_runtime13 = require("react/jsx-runtime");
1861
+ function VideoEditor({ controller: given, options, editable = true, pxPerSecond, onPxPerSecondChange, resolution, showIssues = true, showTranscript = true, showScopes = false, effects, showEffects, className = "" }) {
1154
1862
  const { controller, state } = useVideoEditor(given ?? options ?? {});
1155
- (0, import_react8.useEffect)(() => {
1863
+ useEffects(controller, effects);
1864
+ const effectsShown = showEffects ?? !!(state.effectsProvider || state.project.tracks.some((t) => t.clips.some((c) => c.type === "effect" || c.effects?.length || c.transition?.type === "effect")));
1865
+ (0, import_react14.useEffect)(() => {
1156
1866
  if (editable) return;
1157
1867
  const was = controller.getState().readOnly;
1158
1868
  controller.setReadOnly(true);
1159
1869
  return () => controller.setReadOnly(was);
1160
1870
  }, [controller, editable]);
1161
1871
  const { issues } = state;
1162
- 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)) });
1163
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: `flex flex-col gap-3 ${className}`, "data-video-editor": "", children: [
1164
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: `grid gap-3 ${editable ? "lg:grid-cols-[16rem_minmax(0,1fr)_18rem]" : ""}`, children: [
1165
- editable && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(MediaBin, { controller, className: "min-w-0" }),
1166
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex min-w-0 flex-col gap-2", children: [
1167
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(VideoPreview, { controller, resolution, className: "rounded-md border border-border" }),
1168
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(VideoTransport, { controller, editable }),
1169
- showScopes && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(VideoScopes, { controller })
1872
+ const issueList = showIssues && issues.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime13.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_runtime13.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)) });
1873
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: `flex flex-col gap-3 ${className}`, "data-video-editor": "", children: [
1874
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: `grid gap-3 ${editable ? "lg:grid-cols-[16rem_minmax(0,1fr)_18rem]" : ""}`, children: [
1875
+ editable && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(MediaBin, { controller, className: "min-w-0" }),
1876
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex min-w-0 flex-col gap-2", children: [
1877
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(VideoPreview, { controller, resolution, className: "rounded-md border border-border" }),
1878
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(VideoTransport, { controller, editable }),
1879
+ showScopes && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(VideoScopes, { controller })
1170
1880
  ] }),
1171
- editable && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ClipInspector, { controller, className: "min-w-0" })
1881
+ editable && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex min-w-0 flex-col gap-3", children: [
1882
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ClipInspector, { controller, className: "min-w-0" }),
1883
+ effectsShown && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(VideoEffectsPanel, { controller, className: "min-w-0" })
1884
+ ] })
1172
1885
  ] }),
1173
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(VideoTimeline, { controller, editable, pxPerSecond, onPxPerSecondChange }),
1174
- showTranscript && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(TranscriptPanel, { controller, editable }),
1886
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(VideoTimeline, { controller, editable, pxPerSecond, onPxPerSecondChange }),
1887
+ showTranscript && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(TranscriptPanel, { controller, editable }),
1175
1888
  issueList
1176
1889
  ] });
1177
1890
  }
@@ -1181,6 +1894,7 @@ function VideoEditor({ controller: given, options, editable = true, pxPerSecond,
1181
1894
  MediaBin,
1182
1895
  TranscriptPanel,
1183
1896
  VideoEditor,
1897
+ VideoEffectsPanel,
1184
1898
  VideoPreview,
1185
1899
  VideoScopes,
1186
1900
  VideoTimeline,
@@ -1188,6 +1902,7 @@ function VideoEditor({ controller: given, options, editable = true, pxPerSecond,
1188
1902
  useVideoEditor,
1189
1903
  useVideoEditorController,
1190
1904
  useVideoEditorState,
1191
- useVideoEditorTools
1905
+ useVideoEditorTools,
1906
+ useVideoEffectsTools
1192
1907
  });
1193
1908
  //# sourceMappingURL=index.cjs.map