@pixerate/editor-react 0.77.0 → 0.78.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.
@@ -1,6 +1,7 @@
1
- import { VideoEditorController, VideoEditorState, VideoEditResult, VideoEditorOptions, VideoEditorToolProviderOptions, VideoPreviewOptions, VideoScopeKind, VideoWaveformMode, probeVideoMedia } from '@pixerate/editor/video-editor';
1
+ import { VideoEditorController, VideoEditorState, VideoEditResult, VideoEditorOptions, VideoEditorToolProviderOptions, VideoEffectsToolProviderOptions, VideoPreviewOptions, VideoScopeKind, VideoWaveformMode, probeVideoMedia } from '@pixerate/editor/video-editor';
2
2
  import { ToolRegistry } from '@pixerate/editor/tools';
3
3
  import React__default from 'react';
4
+ import { EffectProvider } from '@pixerate/editor/effects';
4
5
 
5
6
  /** A `VideoEditorController` created once for the component's lifetime (destroyed on unmount). */
6
7
  declare function useVideoEditorController(options?: VideoEditorOptions): VideoEditorController;
@@ -35,6 +36,13 @@ declare function useVideoEditor(controllerOrOptions?: VideoEditorController | Vi
35
36
  * `trim_video_clip`, …) in `registry` while mounted.
36
37
  */
37
38
  declare function useVideoEditorTools(registry: ToolRegistry | null | undefined, controller: VideoEditorController, options?: VideoEditorToolProviderOptions): void;
39
+ /**
40
+ * Registers the video editor's effect tools (`search_effects`, `describe_effect`, `apply_effect`,
41
+ * `add_video_effect_clip`, `set_video_effect_transition`, …) in `registry` while mounted. They
42
+ * work once an effects provider is registered (see `useEffects`); before that the read tools say
43
+ * so and the write tools refuse.
44
+ */
45
+ declare function useVideoEffectsTools(registry: ToolRegistry | null | undefined, controller: VideoEditorController, options?: VideoEffectsToolProviderOptions): void;
38
46
 
39
47
  interface VideoPreviewProps {
40
48
  controller: VideoEditorController;
@@ -163,6 +171,14 @@ interface VideoEditorProps {
163
171
  showTranscript?: boolean;
164
172
  /** Show colour scopes (histogram, luma waveform, vectorscope) of the preview under it (default false). */
165
173
  showScopes?: boolean;
174
+ /**
175
+ * An effects provider (see `@pixerate/editor/effects`), registered with the controller while
176
+ * mounted: clip effects, effect transitions and effect clips, with the effects panel. Optional:
177
+ * without one the editor is as before (effects in a project show as placeholders).
178
+ */
179
+ effects?: EffectProvider | null;
180
+ /** Show the effects panel (default: when a provider is given or the project has effects). */
181
+ showEffects?: boolean;
166
182
  className?: string;
167
183
  }
168
184
  /**
@@ -170,6 +186,18 @@ interface VideoEditorProps {
170
186
  * transport, the timeline and the clip inspector, with the project's issues listed under the
171
187
  * timeline. Every edit is one undo step; the controller's agent tools edit the same project.
172
188
  */
173
- declare function VideoEditor({ controller: given, options, editable, pxPerSecond, onPxPerSecondChange, resolution, showIssues, showTranscript, showScopes, className }: VideoEditorProps): React__default.JSX.Element;
189
+ declare function VideoEditor({ controller: given, options, editable, pxPerSecond, onPxPerSecondChange, resolution, showIssues, showTranscript, showScopes, effects, showEffects, className }: VideoEditorProps): React__default.JSX.Element;
190
+
191
+ interface VideoEffectsPanelProps {
192
+ controller: VideoEditorController;
193
+ className?: string;
194
+ }
195
+ /**
196
+ * Provider effects for the selected clip: its effect stack (add, turn on and off, reorder, change
197
+ * controls, remove), its effect transition, and an effect clip's own controls; plus adding effect
198
+ * clips at the playhead. Every change is one undo step (a slider drag is one). Without a provider it
199
+ * lists what the project holds and says why nothing can be added.
200
+ */
201
+ declare function VideoEffectsPanel({ controller, className }: VideoEffectsPanelProps): React__default.JSX.Element;
174
202
 
175
- export { ClipInspector, type ClipInspectorProps, MediaBin, type MediaBinProps, TranscriptPanel, type TranscriptPanelProps, type UseVideoEditorResult, VideoEditor, type VideoEditorProps, VideoPreview, type VideoPreviewProps, VideoScopes, type VideoScopesProps, VideoTimeline, type VideoTimelineProps, VideoTransport, type VideoTransportProps, useVideoEditor, useVideoEditorController, useVideoEditorState, useVideoEditorTools };
203
+ export { ClipInspector, type ClipInspectorProps, MediaBin, type MediaBinProps, TranscriptPanel, type TranscriptPanelProps, type UseVideoEditorResult, VideoEditor, type VideoEditorProps, VideoEffectsPanel, type VideoEffectsPanelProps, VideoPreview, type VideoPreviewProps, VideoScopes, type VideoScopesProps, VideoTimeline, type VideoTimelineProps, VideoTransport, type VideoTransportProps, useVideoEditor, useVideoEditorController, useVideoEditorState, useVideoEditorTools, useVideoEffectsTools };
@@ -1,6 +1,7 @@
1
- import { VideoEditorController, VideoEditorState, VideoEditResult, VideoEditorOptions, VideoEditorToolProviderOptions, VideoPreviewOptions, VideoScopeKind, VideoWaveformMode, probeVideoMedia } from '@pixerate/editor/video-editor';
1
+ import { VideoEditorController, VideoEditorState, VideoEditResult, VideoEditorOptions, VideoEditorToolProviderOptions, VideoEffectsToolProviderOptions, VideoPreviewOptions, VideoScopeKind, VideoWaveformMode, probeVideoMedia } from '@pixerate/editor/video-editor';
2
2
  import { ToolRegistry } from '@pixerate/editor/tools';
3
3
  import React__default from 'react';
4
+ import { EffectProvider } from '@pixerate/editor/effects';
4
5
 
5
6
  /** A `VideoEditorController` created once for the component's lifetime (destroyed on unmount). */
6
7
  declare function useVideoEditorController(options?: VideoEditorOptions): VideoEditorController;
@@ -35,6 +36,13 @@ declare function useVideoEditor(controllerOrOptions?: VideoEditorController | Vi
35
36
  * `trim_video_clip`, …) in `registry` while mounted.
36
37
  */
37
38
  declare function useVideoEditorTools(registry: ToolRegistry | null | undefined, controller: VideoEditorController, options?: VideoEditorToolProviderOptions): void;
39
+ /**
40
+ * Registers the video editor's effect tools (`search_effects`, `describe_effect`, `apply_effect`,
41
+ * `add_video_effect_clip`, `set_video_effect_transition`, …) in `registry` while mounted. They
42
+ * work once an effects provider is registered (see `useEffects`); before that the read tools say
43
+ * so and the write tools refuse.
44
+ */
45
+ declare function useVideoEffectsTools(registry: ToolRegistry | null | undefined, controller: VideoEditorController, options?: VideoEffectsToolProviderOptions): void;
38
46
 
39
47
  interface VideoPreviewProps {
40
48
  controller: VideoEditorController;
@@ -163,6 +171,14 @@ interface VideoEditorProps {
163
171
  showTranscript?: boolean;
164
172
  /** Show colour scopes (histogram, luma waveform, vectorscope) of the preview under it (default false). */
165
173
  showScopes?: boolean;
174
+ /**
175
+ * An effects provider (see `@pixerate/editor/effects`), registered with the controller while
176
+ * mounted: clip effects, effect transitions and effect clips, with the effects panel. Optional:
177
+ * without one the editor is as before (effects in a project show as placeholders).
178
+ */
179
+ effects?: EffectProvider | null;
180
+ /** Show the effects panel (default: when a provider is given or the project has effects). */
181
+ showEffects?: boolean;
166
182
  className?: string;
167
183
  }
168
184
  /**
@@ -170,6 +186,18 @@ interface VideoEditorProps {
170
186
  * transport, the timeline and the clip inspector, with the project's issues listed under the
171
187
  * timeline. Every edit is one undo step; the controller's agent tools edit the same project.
172
188
  */
173
- declare function VideoEditor({ controller: given, options, editable, pxPerSecond, onPxPerSecondChange, resolution, showIssues, showTranscript, showScopes, className }: VideoEditorProps): React__default.JSX.Element;
189
+ declare function VideoEditor({ controller: given, options, editable, pxPerSecond, onPxPerSecondChange, resolution, showIssues, showTranscript, showScopes, effects, showEffects, className }: VideoEditorProps): React__default.JSX.Element;
190
+
191
+ interface VideoEffectsPanelProps {
192
+ controller: VideoEditorController;
193
+ className?: string;
194
+ }
195
+ /**
196
+ * Provider effects for the selected clip: its effect stack (add, turn on and off, reorder, change
197
+ * controls, remove), its effect transition, and an effect clip's own controls; plus adding effect
198
+ * clips at the playhead. Every change is one undo step (a slider drag is one). Without a provider it
199
+ * lists what the project holds and says why nothing can be added.
200
+ */
201
+ declare function VideoEffectsPanel({ controller, className }: VideoEffectsPanelProps): React__default.JSX.Element;
174
202
 
175
- export { ClipInspector, type ClipInspectorProps, MediaBin, type MediaBinProps, TranscriptPanel, type TranscriptPanelProps, type UseVideoEditorResult, VideoEditor, type VideoEditorProps, VideoPreview, type VideoPreviewProps, VideoScopes, type VideoScopesProps, VideoTimeline, type VideoTimelineProps, VideoTransport, type VideoTransportProps, useVideoEditor, useVideoEditorController, useVideoEditorState, useVideoEditorTools };
203
+ export { ClipInspector, type ClipInspectorProps, MediaBin, type MediaBinProps, TranscriptPanel, type TranscriptPanelProps, type UseVideoEditorResult, VideoEditor, type VideoEditorProps, VideoEffectsPanel, type VideoEffectsPanelProps, VideoPreview, type VideoPreviewProps, VideoScopes, type VideoScopesProps, VideoTimeline, type VideoTimelineProps, VideoTransport, type VideoTransportProps, useVideoEditor, useVideoEditorController, useVideoEditorState, useVideoEditorTools, useVideoEffectsTools };
@@ -1,8 +1,17 @@
1
+ import {
2
+ EffectParameters,
3
+ EffectsBrowser,
4
+ useEffectDescription,
5
+ useEffects
6
+ } from "../chunk-FX3EREF5.js";
7
+ import "../chunk-ZQZAKUUM.js";
8
+
1
9
  // src/video-editor/useVideoEditor.ts
2
10
  import { useEffect, useMemo, useRef, useSyncExternalStore } from "react";
3
11
  import {
4
12
  VideoEditorController,
5
- createVideoEditorToolProvider
13
+ createVideoEditorToolProvider,
14
+ createVideoEffectsToolProvider
6
15
  } from "@pixerate/editor/video-editor";
7
16
  function useVideoEditorController(options = {}) {
8
17
  const ref = useRef(null);
@@ -47,6 +56,13 @@ function useVideoEditorTools(registry, controller, options = {}) {
47
56
  return registry.register(createVideoEditorToolProvider(controller, { id, namespace, readOnly, rasterize, framePixels, framePixelsTimeout }));
48
57
  }, [registry, controller, id, namespace, readOnly, rasterize, framePixels, framePixelsTimeout]);
49
58
  }
59
+ function useVideoEffectsTools(registry, controller, options = {}) {
60
+ const { id, namespace, readOnly } = options;
61
+ useEffect(() => {
62
+ if (!registry) return;
63
+ return registry.register(createVideoEffectsToolProvider(controller, { id, namespace, readOnly }));
64
+ }, [registry, controller, id, namespace, readOnly]);
65
+ }
50
66
 
51
67
  // src/video-editor/VideoPreview.tsx
52
68
  import { useEffect as useEffect2, useRef as useRef2 } from "react";
@@ -823,6 +839,7 @@ function ClipInspector({ controller, className = "" }) {
823
839
  ] }) }),
824
840
  /* @__PURE__ */ jsx5(Row, { label: "Muted", name: "muted", children: /* @__PURE__ */ jsx5("input", { type: "checkbox", className: "justify-self-start", checked: !!clip.muted, disabled, "aria-label": "Muted", onChange: (e) => set("muted", e.target.checked || void 0, e.target.checked ? "Mute clip" : "Unmute clip") }) })
825
841
  ] }),
842
+ clip.type === "effect" && /* @__PURE__ */ jsx5(Row, { label: "Effect", name: "effect", children: /* @__PURE__ */ jsx5("span", { className: "truncate text-xs", title: "Change its controls in the Effects panel", children: clip.name ?? clip.effect }) }),
826
843
  clip.type === "color" && /* @__PURE__ */ jsx5(Row, { label: "Colour", name: "color", children: /* @__PURE__ */ jsx5(Commit, { value: clip.color, "aria-label": "Colour", disabled, onCommit: (raw) => raw.trim() && set("color", raw.trim(), "Change colour") }) }),
827
844
  /* @__PURE__ */ jsx5(Row, { label: "Transition in", name: "transition", children: /* @__PURE__ */ jsxs4(
828
845
  "select",
@@ -833,14 +850,15 @@ function ClipInspector({ controller, className = "" }) {
833
850
  "aria-label": "Transition",
834
851
  onChange: (e) => {
835
852
  const type = e.target.value;
853
+ const { effect: _effect, ...rest } = transition ?? { duration: DEFAULT_TRANSITION };
836
854
  controller.apply(
837
- { 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 },
838
856
  type ? "Set transition" : "Remove transition"
839
857
  );
840
858
  },
841
859
  children: [
842
860
  /* @__PURE__ */ jsx5("option", { value: "", children: "None" }),
843
- VIDEO_TRANSITION_TYPES.map((t) => /* @__PURE__ */ jsx5("option", { value: t, children: VIDEO_TRANSITION_LABELS[t] }, t))
861
+ VIDEO_TRANSITION_TYPES.filter((t) => t !== "effect" || transition?.type === "effect").map((t) => /* @__PURE__ */ jsx5("option", { value: t, children: t === "effect" ? `Effect: ${transition?.effect?.name ?? transition?.effect?.effect}` : VIDEO_TRANSITION_LABELS[t] }, t))
844
862
  ]
845
863
  }
846
864
  ) }),
@@ -1155,9 +1173,153 @@ function TranscriptPanel({ controller, track, editable = true, className = "" })
1155
1173
 
1156
1174
  // src/video-editor/VideoEditor.tsx
1157
1175
  import { useEffect as useEffect5 } from "react";
1158
- import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1159
- function VideoEditor({ controller: given, options, editable = true, pxPerSecond, onPxPerSecondChange, resolution, showIssues = true, showTranscript = true, showScopes = false, className = "" }) {
1176
+
1177
+ // src/video-editor/VideoEffectsPanel.tsx
1178
+ import { useState as useState6 } from "react";
1179
+ import {
1180
+ findVideoClip as findVideoClip2,
1181
+ isVideoKeyframes
1182
+ } from "@pixerate/editor/video-editor";
1183
+ import { Fragment as Fragment6, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1184
+ var btn5 = "rounded border border-border px-1.5 py-0.5 text-[11px] disabled:opacity-40";
1185
+ var keyedOf = (values) => new Set(Object.entries(values ?? {}).filter(([, v]) => isVideoKeyframes(v)).map(([k]) => k));
1186
+ function EffectControls({
1187
+ provider,
1188
+ effect,
1189
+ name,
1190
+ values,
1191
+ disabled,
1192
+ onChange,
1193
+ onPreset
1194
+ }) {
1195
+ const { description, error } = useEffectDescription(provider, effect);
1196
+ if (!provider) return /* @__PURE__ */ jsxs7("p", { className: "text-[11px] text-muted-foreground", children: [
1197
+ 'No effects provider: "',
1198
+ name ?? effect,
1199
+ '" is kept and shown as a placeholder.'
1200
+ ] });
1201
+ if (error) return /* @__PURE__ */ jsx8("p", { className: "text-[11px] text-amber-500", children: error });
1202
+ if (!description) return /* @__PURE__ */ jsx8("p", { className: "text-[11px] text-muted-foreground", children: "Loading\u2026" });
1203
+ return /* @__PURE__ */ jsx8(EffectParameters, { description, values: values ?? {}, keyframed: keyedOf(values), disabled, onChange, onReset: (k) => onChange(k, description.defaults[k], null), onPreset });
1204
+ }
1205
+ function VideoEffectsPanel({ controller, className = "" }) {
1206
+ const state = useVideoEditorState(controller);
1207
+ const provider = state.effectsProvider;
1208
+ const [browse, setBrowse] = useState6(null);
1209
+ const [message, setMessage] = useState6(null);
1210
+ const [open, setOpen] = useState6(null);
1211
+ const selected = state.selection[0];
1212
+ const at = selected ? findVideoClip2(state.project, selected) : null;
1213
+ const clip = at?.clip ?? null;
1214
+ const disabled = state.readOnly;
1215
+ const report = (r) => void Promise.resolve(r).then((x) => setMessage(x.ok ? null : x.error));
1216
+ const addEffectClip = (effect) => {
1217
+ const track = at && at.track.kind === "video" ? at.track.id : state.project.tracks.find((t) => t.kind === "video")?.id;
1218
+ if (!track) return setMessage("Add a video track first.");
1219
+ report(controller.addEffectClip(track, effect, { start: Math.round(state.time), mode: "overwrite" }).then((r) => (r.ok && r.clip && controller.select([r.clip]), r)));
1220
+ };
1221
+ return /* @__PURE__ */ jsxs7("section", { className: `flex flex-col gap-2 rounded-md border border-border p-2 text-xs ${className}`, "aria-label": "Effects", "data-video-effects": "", children: [
1222
+ /* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-1", children: [
1223
+ /* @__PURE__ */ jsx8("h3", { className: "flex-1 font-semibold", children: "Effects" }),
1224
+ /* @__PURE__ */ jsx8("button", { type: "button", className: btn5, disabled: disabled || !provider, onClick: () => setBrowse(browse === "generator" ? null : "generator"), "aria-pressed": browse === "generator", children: "Add effect clip" })
1225
+ ] }),
1226
+ !provider && /* @__PURE__ */ jsx8("p", { className: "text-[11px] text-muted-foreground", children: "No effects provider is registered. Effects in the project are kept and drawn as placeholders." }),
1227
+ browse === "generator" && /* @__PURE__ */ jsx8(EffectsBrowser, { provider, uses: ["generator", "title", "overlay"], title: "Generators and titles", onPick: (e) => (setBrowse(null), addEffectClip(e.id)) }),
1228
+ !clip && /* @__PURE__ */ jsx8("p", { className: "text-[11px] text-muted-foreground", children: "Select a clip to see its effects." }),
1229
+ clip && /* @__PURE__ */ jsxs7(Fragment6, { children: [
1230
+ clip.type === "effect" && /* @__PURE__ */ jsxs7("div", { className: "space-y-1", "data-effect-part": "generator", children: [
1231
+ /* @__PURE__ */ jsx8("div", { className: "font-medium", children: clip.name ?? clip.effect }),
1232
+ /* @__PURE__ */ jsx8(
1233
+ EffectControls,
1234
+ {
1235
+ provider,
1236
+ effect: clip.effect,
1237
+ name: clip.name,
1238
+ values: clip.values,
1239
+ disabled,
1240
+ onChange: (k, v, gesture) => report(controller.updateEffectValues(clip.id, "generator", { [k]: v }, { merge: gesture ?? void 0 })),
1241
+ onPreset: (values) => report(controller.updateEffectValues(clip.id, "generator", values))
1242
+ }
1243
+ )
1244
+ ] }),
1245
+ /* @__PURE__ */ jsxs7("div", { className: "space-y-1", "data-effect-part": "transition", children: [
1246
+ /* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-1", children: [
1247
+ /* @__PURE__ */ jsx8("span", { className: "flex-1 font-medium", children: "Transition in" }),
1248
+ clip.transition?.type === "effect" ? /* @__PURE__ */ jsx8("button", { type: "button", className: btn5, disabled, onClick: () => report(controller.apply({ op: "set_transition", clip: clip.id, transition: null }, "Remove transition")), children: "Remove" }) : /* @__PURE__ */ jsx8("button", { type: "button", className: btn5, disabled: disabled || !provider, "aria-pressed": browse === "transition", onClick: () => setBrowse(browse === "transition" ? null : "transition"), children: "Effect transition\u2026" })
1249
+ ] }),
1250
+ browse === "transition" && /* @__PURE__ */ jsx8(EffectsBrowser, { provider, uses: ["transition"], title: "Transitions", onPick: (e) => (setBrowse(null), report(controller.setEffectTransition(clip.id, e.id))) }),
1251
+ clip.transition?.type === "effect" && clip.transition.effect && /* @__PURE__ */ jsx8(
1252
+ EffectControls,
1253
+ {
1254
+ provider,
1255
+ effect: clip.transition.effect.effect,
1256
+ name: clip.transition.effect.name,
1257
+ values: clip.transition.effect.values,
1258
+ disabled,
1259
+ onChange: (k, v, gesture) => report(controller.updateEffectValues(clip.id, "transition", { [k]: v }, { merge: gesture ?? void 0 })),
1260
+ onPreset: (values) => report(controller.updateEffectValues(clip.id, "transition", values))
1261
+ }
1262
+ )
1263
+ ] }),
1264
+ /* @__PURE__ */ jsxs7("div", { className: "space-y-1", "data-effect-part": "stack", children: [
1265
+ /* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-1", children: [
1266
+ /* @__PURE__ */ jsx8("span", { className: "flex-1 font-medium", children: "On this clip" }),
1267
+ /* @__PURE__ */ jsx8("button", { type: "button", className: btn5, disabled: disabled || !provider, "aria-pressed": browse === "clip", onClick: () => setBrowse(browse === "clip" ? null : "clip"), children: "Add effect\u2026" })
1268
+ ] }),
1269
+ browse === "clip" && /* @__PURE__ */ jsx8(EffectsBrowser, { provider, uses: ["filter", "overlay"], title: "Filters and overlays", onPick: (e) => (setBrowse(null), report(controller.applyClipEffect(clip.id, e.id))) }),
1270
+ /* @__PURE__ */ jsx8("ol", { className: "space-y-1", children: (clip.effects ?? []).map((e, i, all) => /* @__PURE__ */ jsxs7("li", { className: "rounded border border-border p-1", "data-clip-effect": e.id, children: [
1271
+ /* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-1", children: [
1272
+ /* @__PURE__ */ jsx8("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 })) }),
1273
+ /* @__PURE__ */ jsx8("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 }),
1274
+ /* @__PURE__ */ jsx8("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" }),
1275
+ /* @__PURE__ */ jsx8("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" }),
1276
+ /* @__PURE__ */ jsx8("button", { type: "button", className: btn5, disabled, "aria-label": `Remove ${e.name ?? e.effect}`, onClick: () => report(controller.removeClipEffect(clip.id, e.id)), children: "\u2715" })
1277
+ ] }),
1278
+ open === e.id && /* @__PURE__ */ jsxs7("div", { className: "mt-1 space-y-2", children: [
1279
+ /* @__PURE__ */ jsxs7("label", { className: "flex items-center gap-1", children: [
1280
+ /* @__PURE__ */ jsx8("span", { className: "w-14 text-[11px]", children: "Opacity" }),
1281
+ /* @__PURE__ */ jsx8(
1282
+ "input",
1283
+ {
1284
+ type: "range",
1285
+ min: 0,
1286
+ max: 1,
1287
+ step: 0.01,
1288
+ className: "flex-1",
1289
+ disabled: disabled || isVideoKeyframes(e.opacity),
1290
+ value: typeof e.opacity === "number" ? e.opacity : 1,
1291
+ "aria-label": `${e.name ?? e.effect} opacity`,
1292
+ onChange: (ev) => report(controller.updateClipEffect(clip.id, e.id, { opacity: Number(ev.currentTarget.value) }, { merge: `opacity:${clip.id}:${e.id}` }))
1293
+ }
1294
+ )
1295
+ ] }),
1296
+ /* @__PURE__ */ jsx8(
1297
+ EffectControls,
1298
+ {
1299
+ provider,
1300
+ effect: e.effect,
1301
+ name: e.name,
1302
+ values: e.values,
1303
+ disabled,
1304
+ onChange: (k, v, gesture) => report(controller.updateClipEffect(clip.id, e.id, { values: { [k]: v } }, { merge: gesture ?? void 0 })),
1305
+ onPreset: (values) => report(controller.updateClipEffect(clip.id, e.id, { values }))
1306
+ }
1307
+ )
1308
+ ] })
1309
+ ] }, e.id)) }),
1310
+ !clip.effects?.length && /* @__PURE__ */ jsx8("p", { className: "text-[11px] text-muted-foreground", children: "No effects on this clip." })
1311
+ ] })
1312
+ ] }),
1313
+ message && /* @__PURE__ */ jsx8("p", { role: "alert", className: "text-[11px] text-destructive", children: message })
1314
+ ] });
1315
+ }
1316
+
1317
+ // src/video-editor/VideoEditor.tsx
1318
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1319
+ function VideoEditor({ controller: given, options, editable = true, pxPerSecond, onPxPerSecondChange, resolution, showIssues = true, showTranscript = true, showScopes = false, effects, showEffects, className = "" }) {
1160
1320
  const { controller, state } = useVideoEditor(given ?? options ?? {});
1321
+ useEffects(controller, effects);
1322
+ const effectsShown = showEffects ?? !!(state.effectsProvider || state.project.tracks.some((t) => t.clips.some((c) => c.type === "effect" || c.effects?.length || c.transition?.type === "effect")));
1161
1323
  useEffect5(() => {
1162
1324
  if (editable) return;
1163
1325
  const was = controller.getState().readOnly;
@@ -1165,19 +1327,22 @@ function VideoEditor({ controller: given, options, editable = true, pxPerSecond,
1165
1327
  return () => controller.setReadOnly(was);
1166
1328
  }, [controller, editable]);
1167
1329
  const { issues } = state;
1168
- const issueList = showIssues && issues.length > 0 && /* @__PURE__ */ jsx8("ul", { className: "space-y-0.5 text-xs", "aria-label": "Video issues", "data-video-issues": "", children: issues.map((issue, i) => /* @__PURE__ */ jsx8("li", { className: issue.severity === "error" ? "text-red-500" : issue.severity === "warning" ? "text-amber-500" : "text-muted-foreground", "data-issue": issue.code, children: issue.message }, i)) });
1169
- return /* @__PURE__ */ jsxs7("div", { className: `flex flex-col gap-3 ${className}`, "data-video-editor": "", children: [
1170
- /* @__PURE__ */ jsxs7("div", { className: `grid gap-3 ${editable ? "lg:grid-cols-[16rem_minmax(0,1fr)_18rem]" : ""}`, children: [
1171
- editable && /* @__PURE__ */ jsx8(MediaBin, { controller, className: "min-w-0" }),
1172
- /* @__PURE__ */ jsxs7("div", { className: "flex min-w-0 flex-col gap-2", children: [
1173
- /* @__PURE__ */ jsx8(VideoPreview, { controller, resolution, className: "rounded-md border border-border" }),
1174
- /* @__PURE__ */ jsx8(VideoTransport, { controller, editable }),
1175
- showScopes && /* @__PURE__ */ jsx8(VideoScopes, { controller })
1330
+ const issueList = showIssues && issues.length > 0 && /* @__PURE__ */ jsx9("ul", { className: "space-y-0.5 text-xs", "aria-label": "Video issues", "data-video-issues": "", children: issues.map((issue, i) => /* @__PURE__ */ jsx9("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)) });
1331
+ return /* @__PURE__ */ jsxs8("div", { className: `flex flex-col gap-3 ${className}`, "data-video-editor": "", children: [
1332
+ /* @__PURE__ */ jsxs8("div", { className: `grid gap-3 ${editable ? "lg:grid-cols-[16rem_minmax(0,1fr)_18rem]" : ""}`, children: [
1333
+ editable && /* @__PURE__ */ jsx9(MediaBin, { controller, className: "min-w-0" }),
1334
+ /* @__PURE__ */ jsxs8("div", { className: "flex min-w-0 flex-col gap-2", children: [
1335
+ /* @__PURE__ */ jsx9(VideoPreview, { controller, resolution, className: "rounded-md border border-border" }),
1336
+ /* @__PURE__ */ jsx9(VideoTransport, { controller, editable }),
1337
+ showScopes && /* @__PURE__ */ jsx9(VideoScopes, { controller })
1176
1338
  ] }),
1177
- editable && /* @__PURE__ */ jsx8(ClipInspector, { controller, className: "min-w-0" })
1339
+ editable && /* @__PURE__ */ jsxs8("div", { className: "flex min-w-0 flex-col gap-3", children: [
1340
+ /* @__PURE__ */ jsx9(ClipInspector, { controller, className: "min-w-0" }),
1341
+ effectsShown && /* @__PURE__ */ jsx9(VideoEffectsPanel, { controller, className: "min-w-0" })
1342
+ ] })
1178
1343
  ] }),
1179
- /* @__PURE__ */ jsx8(VideoTimeline, { controller, editable, pxPerSecond, onPxPerSecondChange }),
1180
- showTranscript && /* @__PURE__ */ jsx8(TranscriptPanel, { controller, editable }),
1344
+ /* @__PURE__ */ jsx9(VideoTimeline, { controller, editable, pxPerSecond, onPxPerSecondChange }),
1345
+ showTranscript && /* @__PURE__ */ jsx9(TranscriptPanel, { controller, editable }),
1181
1346
  issueList
1182
1347
  ] });
1183
1348
  }
@@ -1186,6 +1351,7 @@ export {
1186
1351
  MediaBin,
1187
1352
  TranscriptPanel,
1188
1353
  VideoEditor,
1354
+ VideoEffectsPanel,
1189
1355
  VideoPreview,
1190
1356
  VideoScopes,
1191
1357
  VideoTimeline,
@@ -1193,6 +1359,7 @@ export {
1193
1359
  useVideoEditor,
1194
1360
  useVideoEditorController,
1195
1361
  useVideoEditorState,
1196
- useVideoEditorTools
1362
+ useVideoEditorTools,
1363
+ useVideoEffectsTools
1197
1364
  };
1198
1365
  //# sourceMappingURL=index.js.map