@tangle-network/agent-app 0.11.0 → 0.12.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.
Files changed (42) hide show
  1. package/dist/TimelineEditor-OXPJZDP2.js +12 -0
  2. package/dist/TimelineEditor-OXPJZDP2.js.map +1 -0
  3. package/dist/apply-Cp8c3K9D.d.ts +249 -0
  4. package/dist/billing/index.js +1 -1
  5. package/dist/chunk-3WAJWYKD.js +1730 -0
  6. package/dist/chunk-3WAJWYKD.js.map +1 -0
  7. package/dist/chunk-CF5DZELC.js +111 -0
  8. package/dist/chunk-CF5DZELC.js.map +1 -0
  9. package/dist/{chunk-4YTWB5MG.js → chunk-ETX4O4BB.js} +98 -1
  10. package/dist/chunk-ETX4O4BB.js.map +1 -0
  11. package/dist/{chunk-YS6A6G57.js → chunk-G3HCU7TA.js} +37 -4
  12. package/dist/chunk-G3HCU7TA.js.map +1 -0
  13. package/dist/chunk-IHR6K3GF.js +2367 -0
  14. package/dist/chunk-IHR6K3GF.js.map +1 -0
  15. package/dist/{chunk-OLCVUGGI.js → chunk-IJZJWKUK.js} +1 -61
  16. package/dist/chunk-IJZJWKUK.js.map +1 -0
  17. package/dist/{chunk-EYXTDVDY.js → chunk-SAOAAA3S.js} +2 -2
  18. package/dist/chunk-ZYBWGSAZ.js +130 -0
  19. package/dist/chunk-ZYBWGSAZ.js.map +1 -0
  20. package/dist/index.d.ts +6 -2
  21. package/dist/index.js +130 -8
  22. package/dist/mcp-CIupfjxV.d.ts +112 -0
  23. package/dist/preset-cloudflare/index.js +2 -2
  24. package/dist/runtime/index.d.ts +108 -1
  25. package/dist/runtime/index.js +7 -1
  26. package/dist/sequences/drizzle.d.ts +1244 -0
  27. package/dist/sequences/drizzle.js +368 -0
  28. package/dist/sequences/drizzle.js.map +1 -0
  29. package/dist/sequences/index.d.ts +327 -0
  30. package/dist/sequences/index.js +114 -0
  31. package/dist/sequences/index.js.map +1 -0
  32. package/dist/sequences-react/index.d.ts +752 -0
  33. package/dist/sequences-react/index.js +241 -0
  34. package/dist/sequences-react/index.js.map +1 -0
  35. package/dist/store-gckrNq-g.d.ts +242 -0
  36. package/dist/tools/index.d.ts +24 -108
  37. package/dist/tools/index.js +12 -6
  38. package/package.json +37 -2
  39. package/dist/chunk-4YTWB5MG.js.map +0 -1
  40. package/dist/chunk-OLCVUGGI.js.map +0 -1
  41. package/dist/chunk-YS6A6G57.js.map +0 -1
  42. /package/dist/{chunk-EYXTDVDY.js.map → chunk-SAOAAA3S.js.map} +0 -0
@@ -0,0 +1,2367 @@
1
+ import {
2
+ MIN_SEQUENCE_CLIP_FRAMES,
3
+ assertClipFitsSequence,
4
+ chooseCaptionPlacement,
5
+ clampClipStart,
6
+ formatTimecode,
7
+ framesToSeconds,
8
+ secondsToFrames,
9
+ snapshotFrame,
10
+ trackIntervals
11
+ } from "./chunk-ZYBWGSAZ.js";
12
+
13
+ // src/sequences-react/components/TimelineEditor.tsx
14
+ import { useEffect as useEffect3, useMemo as useMemo3, useRef as useRef3, useState as useState3, useSyncExternalStore } from "react";
15
+
16
+ // src/sequences-react/engine/command-stack.ts
17
+ var COMMAND_HISTORY_LIMIT = 200;
18
+ function createCommandStack(initial) {
19
+ let state = {
20
+ timeline: initial,
21
+ playheadFrame: 0,
22
+ selectedClipIds: [],
23
+ zoom: 1,
24
+ scrollLeft: 0
25
+ };
26
+ const undoStack = [];
27
+ const redoStack = [];
28
+ const listeners = /* @__PURE__ */ new Set();
29
+ const notify = () => {
30
+ for (const listener of [...listeners]) listener();
31
+ };
32
+ return {
33
+ execute(command) {
34
+ state = command.execute(state);
35
+ undoStack.push(command);
36
+ if (undoStack.length > COMMAND_HISTORY_LIMIT) {
37
+ undoStack.splice(0, undoStack.length - COMMAND_HISTORY_LIMIT);
38
+ }
39
+ redoStack.length = 0;
40
+ notify();
41
+ },
42
+ // Both transforms run BEFORE the stacks move: a throwing transform (the
43
+ // documented missing-clip path after reset()) leaves history and state
44
+ // exactly as they were, so the entry is never silently destroyed and the
45
+ // caller can retry after the next refresh restores the target.
46
+ undo() {
47
+ const command = undoStack[undoStack.length - 1];
48
+ if (!command) throw new Error("nothing to undo \u2014 guard with canUndo() before calling undo()");
49
+ state = command.undo(state);
50
+ undoStack.pop();
51
+ redoStack.push(command);
52
+ notify();
53
+ },
54
+ redo() {
55
+ const command = redoStack[redoStack.length - 1];
56
+ if (!command) throw new Error("nothing to redo \u2014 guard with canRedo() before calling redo()");
57
+ state = command.execute(state);
58
+ redoStack.pop();
59
+ undoStack.push(command);
60
+ notify();
61
+ },
62
+ canUndo() {
63
+ return undoStack.length > 0;
64
+ },
65
+ canRedo() {
66
+ return redoStack.length > 0;
67
+ },
68
+ subscribe(listener) {
69
+ listeners.add(listener);
70
+ return () => {
71
+ listeners.delete(listener);
72
+ };
73
+ },
74
+ getState() {
75
+ return state;
76
+ },
77
+ /** Rebase onto a server-refreshed timeline. History survives (see module
78
+ * header); selection drops ids the refresh removed and the playhead
79
+ * clamps into the new duration so view state never dangles. */
80
+ reset(timeline) {
81
+ const liveClipIds = new Set(timeline.clips.map((clip) => clip.id));
82
+ state = {
83
+ ...state,
84
+ timeline,
85
+ playheadFrame: Math.max(0, Math.min(state.playheadFrame, timeline.sequence.durationFrames - 1)),
86
+ selectedClipIds: state.selectedClipIds.filter((id) => liveClipIds.has(id))
87
+ };
88
+ notify();
89
+ }
90
+ };
91
+ }
92
+
93
+ // src/sequences-react/engine/commands.ts
94
+ var identityClipId = (clipId) => clipId;
95
+ function requireClip(timeline, clipId, context) {
96
+ const clip = timeline.clips.find((candidate) => candidate.id === clipId);
97
+ if (!clip) throw new Error(`${context}: clip ${clipId} does not exist in sequence ${timeline.sequence.id}`);
98
+ return clip;
99
+ }
100
+ function requireUnlockedTrack(timeline, trackId, context) {
101
+ const track = timeline.tracks.find((candidate) => candidate.id === trackId);
102
+ if (!track) throw new Error(`${context}: track ${trackId} does not exist in sequence ${timeline.sequence.id}`);
103
+ if (track.locked) throw new Error(`${context}: track ${track.name} (${trackId}) is locked`);
104
+ return track;
105
+ }
106
+ function assertNewClipId(timeline, clipId, context) {
107
+ if (timeline.clips.some((candidate) => candidate.id === clipId)) {
108
+ throw new Error(`${context}: clip id ${clipId} already exists in sequence ${timeline.sequence.id}`);
109
+ }
110
+ }
111
+ function patchClip(state, clipId, context, patch) {
112
+ requireClip(state.timeline, clipId, context);
113
+ return {
114
+ ...state,
115
+ timeline: {
116
+ ...state.timeline,
117
+ clips: state.timeline.clips.map((clip) => clip.id === clipId ? { ...clip, ...patch } : clip)
118
+ }
119
+ };
120
+ }
121
+ function insertClip(state, clip, context) {
122
+ assertNewClipId(state.timeline, clip.id, context);
123
+ return {
124
+ ...state,
125
+ timeline: { ...state.timeline, clips: [...state.timeline.clips, clip] }
126
+ };
127
+ }
128
+ function removeClip(state, clipId, context) {
129
+ requireClip(state.timeline, clipId, context);
130
+ return {
131
+ ...state,
132
+ timeline: {
133
+ ...state.timeline,
134
+ clips: state.timeline.clips.filter((clip) => clip.id !== clipId)
135
+ },
136
+ selectedClipIds: state.selectedClipIds.filter((id) => id !== clipId)
137
+ };
138
+ }
139
+ function moveClipCommand(input) {
140
+ const context = "move_clip";
141
+ const clip = requireClip(input.timeline, input.clipId, context);
142
+ const targetTrackId = input.trackId ?? clip.trackId;
143
+ requireUnlockedTrack(input.timeline, targetTrackId, context);
144
+ if (!Number.isInteger(input.startFrame)) throw new Error(`${context}: startFrame must be an integer frame`);
145
+ const targetStart = clampClipStart({
146
+ startFrame: input.startFrame,
147
+ durationFrames: clip.durationFrames,
148
+ sequenceDurationFrames: input.timeline.sequence.durationFrames
149
+ });
150
+ const originalStart = clip.startFrame;
151
+ const originalTrackId = clip.trackId;
152
+ const trackChanged = targetTrackId !== originalTrackId;
153
+ const clipId = input.clipId;
154
+ const resolve = input.resolveClipId ?? identityClipId;
155
+ return {
156
+ label: `Move ${clip.label}`,
157
+ execute: (state) => patchClip(state, resolve(clipId), context, { startFrame: targetStart, trackId: targetTrackId }),
158
+ undo: (state) => patchClip(state, resolve(clipId), context, { startFrame: originalStart, trackId: originalTrackId }),
159
+ operations: () => [
160
+ { type: "move_clip", clipId: resolve(clipId), startFrame: targetStart, ...trackChanged ? { trackId: targetTrackId } : {} }
161
+ ],
162
+ inverseOperations: () => [
163
+ { type: "move_clip", clipId: resolve(clipId), startFrame: originalStart, ...trackChanged ? { trackId: originalTrackId } : {} }
164
+ ]
165
+ };
166
+ }
167
+ function trimClipCommand(input) {
168
+ const context = "trim_clip";
169
+ const clip = requireClip(input.timeline, input.clipId, context);
170
+ assertClipFitsSequence({
171
+ startFrame: input.startFrame,
172
+ durationFrames: input.durationFrames,
173
+ sequenceDurationFrames: input.timeline.sequence.durationFrames,
174
+ label: `${context} ${clip.label}`
175
+ });
176
+ if (input.sourceInFrame !== void 0 && (!Number.isInteger(input.sourceInFrame) || input.sourceInFrame < 0)) {
177
+ throw new Error(`${context}: sourceInFrame must be a non-negative integer`);
178
+ }
179
+ const targetSourceIn = input.sourceInFrame ?? clip.sourceInFrame;
180
+ if (clip.sourceOutFrame !== null && targetSourceIn + input.durationFrames > clip.sourceOutFrame) {
181
+ throw new Error(
182
+ `${context}: needs ${input.durationFrames} source frames from ${targetSourceIn} but ${clip.label} has source window [${clip.sourceInFrame}, ${clip.sourceOutFrame})`
183
+ );
184
+ }
185
+ const target = { startFrame: input.startFrame, durationFrames: input.durationFrames, sourceInFrame: targetSourceIn };
186
+ const original = { startFrame: clip.startFrame, durationFrames: clip.durationFrames, sourceInFrame: clip.sourceInFrame };
187
+ const clipId = input.clipId;
188
+ const resolve = input.resolveClipId ?? identityClipId;
189
+ return {
190
+ label: `Trim ${clip.label}`,
191
+ execute: (state) => patchClip(state, resolve(clipId), context, target),
192
+ undo: (state) => patchClip(state, resolve(clipId), context, original),
193
+ operations: () => [{ type: "trim_clip", clipId: resolve(clipId), ...target }],
194
+ inverseOperations: () => [{ type: "trim_clip", clipId: resolve(clipId), ...original }]
195
+ };
196
+ }
197
+ function placeClipCommand(input) {
198
+ const context = "place_clip";
199
+ assertNewClipId(input.timeline, input.clipId, context);
200
+ requireUnlockedTrack(input.timeline, input.trackId, context);
201
+ assertClipFitsSequence({
202
+ startFrame: input.startFrame,
203
+ durationFrames: input.durationFrames,
204
+ sequenceDurationFrames: input.timeline.sequence.durationFrames,
205
+ label: `${context} ${input.label}`
206
+ });
207
+ const sourceInFrame = input.sourceInFrame ?? 0;
208
+ if (!Number.isInteger(sourceInFrame) || sourceInFrame < 0) {
209
+ throw new Error(`${context}: sourceInFrame must be a non-negative integer`);
210
+ }
211
+ const clip = {
212
+ id: input.clipId,
213
+ trackId: input.trackId,
214
+ label: input.label,
215
+ startFrame: input.startFrame,
216
+ durationFrames: input.durationFrames,
217
+ sourceInFrame,
218
+ sourceOutFrame: null,
219
+ disabled: false,
220
+ ...input.media ? { media: { url: input.media.url, kind: input.media.kind } } : {},
221
+ ...input.generationId !== void 0 ? { generationId: input.generationId } : {},
222
+ ...input.assetId !== void 0 ? { assetId: input.assetId } : {},
223
+ metadata: input.metadata ?? {}
224
+ };
225
+ const clipId = input.clipId;
226
+ const resolve = input.resolveClipId ?? identityClipId;
227
+ return {
228
+ label: `Place ${input.label}`,
229
+ execute: (state) => insertClip(state, { ...structuredClone(clip), id: resolve(clipId) }, context),
230
+ undo: (state) => removeClip(state, resolve(clipId), context),
231
+ operations: () => [
232
+ {
233
+ type: "place_clip",
234
+ trackId: clip.trackId,
235
+ label: clip.label,
236
+ startFrame: clip.startFrame,
237
+ durationFrames: clip.durationFrames,
238
+ sourceInFrame,
239
+ ...input.media ? { media: { url: input.media.url, kind: input.media.kind } } : {},
240
+ ...input.generationId !== void 0 ? { generationId: input.generationId } : {},
241
+ ...input.assetId !== void 0 ? { assetId: input.assetId } : {},
242
+ ...input.metadata !== void 0 ? { metadata: input.metadata } : {}
243
+ }
244
+ ],
245
+ inverseOperations: () => [{ type: "delete_clip", clipId: resolve(clipId) }]
246
+ };
247
+ }
248
+ function deleteClipCommand(input) {
249
+ const context = "delete_clip";
250
+ const snapshot = structuredClone(requireClip(input.timeline, input.clipId, context));
251
+ const track = input.timeline.tracks.find((candidate) => candidate.id === snapshot.trackId);
252
+ if (!track) throw new Error(`${context}: clip ${snapshot.id} references unknown track ${snapshot.trackId}`);
253
+ const captionText = track.kind === "caption" && typeof snapshot.text === "string" && snapshot.text.length > 0 ? snapshot.text : null;
254
+ const clipId = input.clipId;
255
+ const resolve = input.resolveClipId ?? identityClipId;
256
+ return {
257
+ label: `Delete ${snapshot.label}`,
258
+ execute: (state) => removeClip(state, resolve(clipId), context),
259
+ undo: (state) => insertClip(state, { ...structuredClone(snapshot), id: resolve(clipId) }, context),
260
+ operations: () => [{ type: "delete_clip", clipId: resolve(clipId) }],
261
+ inverseOperations: () => captionText !== null ? [
262
+ {
263
+ type: "add_caption",
264
+ text: captionText,
265
+ ...snapshot.language !== void 0 ? { language: snapshot.language } : {},
266
+ startFrame: snapshot.startFrame,
267
+ durationFrames: snapshot.durationFrames,
268
+ trackId: snapshot.trackId
269
+ }
270
+ ] : [
271
+ {
272
+ type: "place_clip",
273
+ trackId: snapshot.trackId,
274
+ label: snapshot.label,
275
+ startFrame: snapshot.startFrame,
276
+ durationFrames: snapshot.durationFrames,
277
+ sourceInFrame: snapshot.sourceInFrame,
278
+ ...snapshot.sourceOutFrame !== null ? { sourceOutFrame: snapshot.sourceOutFrame } : {},
279
+ ...snapshot.disabled ? { disabled: true } : {},
280
+ ...snapshot.media ? { media: { url: snapshot.media.url, kind: snapshot.media.kind } } : {},
281
+ ...snapshot.generationId !== void 0 ? { generationId: snapshot.generationId } : {},
282
+ ...snapshot.assetId !== void 0 ? { assetId: snapshot.assetId } : {},
283
+ metadata: structuredClone(snapshot.metadata)
284
+ }
285
+ ]
286
+ };
287
+ }
288
+ function splitClipCommand(input) {
289
+ const context = "split_clip";
290
+ const original = structuredClone(requireClip(input.timeline, input.clipId, context));
291
+ assertNewClipId(input.timeline, input.newClipId, context);
292
+ if (!Number.isInteger(input.atFrame)) throw new Error(`${context}: atFrame must be an integer frame`);
293
+ const clipEnd = original.startFrame + original.durationFrames;
294
+ if (input.atFrame <= original.startFrame || input.atFrame >= clipEnd) {
295
+ throw new Error(
296
+ `${context}: atFrame ${input.atFrame} must fall strictly inside clip ${original.id} [${original.startFrame}, ${clipEnd})`
297
+ );
298
+ }
299
+ const headDurationFrames = input.atFrame - original.startFrame;
300
+ const tailDurationFrames = clipEnd - input.atFrame;
301
+ const tail = {
302
+ ...structuredClone(original),
303
+ id: input.newClipId,
304
+ startFrame: input.atFrame,
305
+ durationFrames: tailDurationFrames,
306
+ sourceInFrame: original.sourceInFrame + headDurationFrames
307
+ };
308
+ const clipId = input.clipId;
309
+ const newClipId = input.newClipId;
310
+ const resolve = input.resolveClipId ?? identityClipId;
311
+ const headSourceOutFrame = original.sourceInFrame + headDurationFrames;
312
+ return {
313
+ label: `Split ${original.label}`,
314
+ execute: (state) => insertClip(
315
+ patchClip(state, resolve(clipId), context, { durationFrames: headDurationFrames, sourceOutFrame: headSourceOutFrame }),
316
+ { ...structuredClone(tail), id: resolve(newClipId) },
317
+ context
318
+ ),
319
+ undo: (state) => patchClip(removeClip(state, resolve(newClipId), context), resolve(clipId), context, {
320
+ ...structuredClone(original),
321
+ id: resolve(clipId)
322
+ }),
323
+ operations: () => [{ type: "split_clip", clipId: resolve(clipId), atFrame: input.atFrame }],
324
+ inverseOperations: () => [
325
+ { type: "delete_clip", clipId: resolve(newClipId) },
326
+ {
327
+ type: "trim_clip",
328
+ clipId: resolve(clipId),
329
+ startFrame: original.startFrame,
330
+ durationFrames: original.durationFrames,
331
+ sourceInFrame: original.sourceInFrame,
332
+ // Restores the pre-split window; without it the head keeps its
333
+ // out-point at the cut while regaining the full duration.
334
+ sourceOutFrame: original.sourceOutFrame
335
+ }
336
+ ]
337
+ };
338
+ }
339
+ function addCaptionCommand(input) {
340
+ const context = "add_caption";
341
+ assertNewClipId(input.timeline, input.clipId, context);
342
+ const track = requireUnlockedTrack(input.timeline, input.trackId, context);
343
+ if (track.kind !== "caption") {
344
+ throw new Error(`${context}: track ${track.name} (${track.id}) is kind ${track.kind}; captions require a caption track`);
345
+ }
346
+ if (typeof input.text !== "string" || input.text.length === 0) {
347
+ throw new Error(`${context}: text must be a non-empty string`);
348
+ }
349
+ assertClipFitsSequence({
350
+ startFrame: input.startFrame,
351
+ durationFrames: input.durationFrames,
352
+ sequenceDurationFrames: input.timeline.sequence.durationFrames,
353
+ label: context
354
+ });
355
+ const clip = {
356
+ id: input.clipId,
357
+ trackId: input.trackId,
358
+ label: input.text,
359
+ startFrame: input.startFrame,
360
+ durationFrames: input.durationFrames,
361
+ sourceInFrame: 0,
362
+ sourceOutFrame: null,
363
+ disabled: false,
364
+ text: input.text,
365
+ ...input.language !== void 0 ? { language: input.language } : {},
366
+ metadata: {}
367
+ };
368
+ const clipId = input.clipId;
369
+ const resolve = input.resolveClipId ?? identityClipId;
370
+ return {
371
+ label: `Add caption`,
372
+ execute: (state) => insertClip(state, { ...structuredClone(clip), id: resolve(clipId) }, context),
373
+ undo: (state) => removeClip(state, resolve(clipId), context),
374
+ operations: () => [
375
+ {
376
+ type: "add_caption",
377
+ text: input.text,
378
+ ...input.language !== void 0 ? { language: input.language } : {},
379
+ startFrame: input.startFrame,
380
+ durationFrames: input.durationFrames,
381
+ trackId: input.trackId
382
+ }
383
+ ],
384
+ inverseOperations: () => [{ type: "delete_clip", clipId: resolve(clipId) }]
385
+ };
386
+ }
387
+ function setClipTextCommand(input) {
388
+ const context = "set_clip_text";
389
+ const clip = requireClip(input.timeline, input.clipId, context);
390
+ if (typeof clip.text !== "string") {
391
+ throw new Error(`${context}: clip ${clip.id} has no text body; create caption text through add_caption`);
392
+ }
393
+ if (typeof input.text !== "string" || input.text.length === 0) {
394
+ throw new Error(`${context}: text must be a non-empty string`);
395
+ }
396
+ const originalText = clip.text;
397
+ const originalLanguage = clip.language;
398
+ const targetLanguage = input.language ?? clip.language;
399
+ const mirrorsLabel = clip.label === clip.text;
400
+ const clipId = input.clipId;
401
+ const resolve = input.resolveClipId ?? identityClipId;
402
+ return {
403
+ label: `Edit caption text`,
404
+ execute: (state) => patchClip(state, resolve(clipId), context, {
405
+ text: input.text,
406
+ language: targetLanguage,
407
+ ...mirrorsLabel ? { label: input.text } : {}
408
+ }),
409
+ undo: (state) => patchClip(state, resolve(clipId), context, {
410
+ text: originalText,
411
+ language: originalLanguage,
412
+ ...mirrorsLabel ? { label: originalText } : {}
413
+ }),
414
+ operations: () => [
415
+ { type: "set_clip_text", clipId: resolve(clipId), text: input.text, ...input.language !== void 0 ? { language: input.language } : {} }
416
+ ],
417
+ inverseOperations: () => [
418
+ {
419
+ type: "set_clip_text",
420
+ clipId: resolve(clipId),
421
+ text: originalText,
422
+ ...originalLanguage !== void 0 ? { language: originalLanguage } : {}
423
+ }
424
+ ]
425
+ };
426
+ }
427
+ function toggleClipDisabledCommand(input) {
428
+ const context = "set_clip_disabled";
429
+ const clip = requireClip(input.timeline, input.clipId, context);
430
+ const original = clip.disabled;
431
+ const target = !original;
432
+ const clipId = input.clipId;
433
+ const resolve = input.resolveClipId ?? identityClipId;
434
+ return {
435
+ label: target ? `Disable ${clip.label}` : `Enable ${clip.label}`,
436
+ execute: (state) => patchClip(state, resolve(clipId), context, { disabled: target }),
437
+ undo: (state) => patchClip(state, resolve(clipId), context, { disabled: original }),
438
+ operations: () => [{ type: "set_clip_disabled", clipId: resolve(clipId), disabled: target }],
439
+ inverseOperations: () => [{ type: "set_clip_disabled", clipId: resolve(clipId), disabled: original }]
440
+ };
441
+ }
442
+
443
+ // src/sequences-react/engine/playback.ts
444
+ function resolveRaf() {
445
+ const g = globalThis;
446
+ if (typeof g.requestAnimationFrame !== "function" || typeof g.cancelAnimationFrame !== "function") {
447
+ throw new Error(
448
+ "PlaybackClock requires requestAnimationFrame/cancelAnimationFrame \u2014 playback runs only in a browser (or a test that stubs both globals)"
449
+ );
450
+ }
451
+ return { request: g.requestAnimationFrame.bind(globalThis), cancel: g.cancelAnimationFrame.bind(globalThis) };
452
+ }
453
+ function now() {
454
+ const perf = globalThis.performance;
455
+ if (!perf || typeof perf.now !== "function") {
456
+ throw new Error("PlaybackClock requires performance.now() \u2014 playback runs only in a browser (or a test that stubs it)");
457
+ }
458
+ return perf.now();
459
+ }
460
+ function createPlaybackClock(config) {
461
+ if (!Number.isInteger(config.fps) || config.fps <= 0) {
462
+ throw new Error(`fps must be a positive integer, got ${config.fps}`);
463
+ }
464
+ if (!Number.isInteger(config.durationFrames) || config.durationFrames < 1) {
465
+ throw new Error(`durationFrames must be a positive integer, got ${config.durationFrames}`);
466
+ }
467
+ const lastFrame = config.durationFrames - 1;
468
+ let frame = 0;
469
+ let playing = false;
470
+ let disposed = false;
471
+ let rafId = null;
472
+ let cancelRaf = null;
473
+ let anchorTime = 0;
474
+ let anchorFrame = 0;
475
+ const listeners = /* @__PURE__ */ new Set();
476
+ const notify = () => {
477
+ for (const listener of [...listeners]) listener(frame);
478
+ };
479
+ const stopLoop = () => {
480
+ if (rafId !== null && cancelRaf) cancelRaf(rafId);
481
+ rafId = null;
482
+ };
483
+ const tick = () => {
484
+ rafId = null;
485
+ if (!playing) return;
486
+ const elapsedMs = now() - anchorTime;
487
+ const advanced = anchorFrame + Math.floor(elapsedMs / 1e3 * config.fps);
488
+ if (advanced >= lastFrame) {
489
+ frame = lastFrame;
490
+ playing = false;
491
+ notify();
492
+ return;
493
+ }
494
+ frame = advanced;
495
+ notify();
496
+ rafId = resolveRaf().request(tick);
497
+ };
498
+ return {
499
+ /** Idempotent while playing. Playing from the final frame restarts at 0 —
500
+ * a play button at the end means "watch again", not a dead control. */
501
+ play() {
502
+ if (disposed) throw new Error("PlaybackClock is disposed");
503
+ if (playing) return;
504
+ const raf = resolveRaf();
505
+ cancelRaf = raf.cancel;
506
+ if (frame >= lastFrame) frame = 0;
507
+ anchorTime = now();
508
+ anchorFrame = frame;
509
+ playing = true;
510
+ rafId = raf.request(tick);
511
+ },
512
+ pause() {
513
+ if (!playing) return;
514
+ playing = false;
515
+ stopLoop();
516
+ },
517
+ /** Clamps into [0, durationFrames - 1]; fractional input rounds to the
518
+ * nearest frame. Re-anchors mid-play so playback continues from the
519
+ * seek target, and notifies so scrubbing drives the playhead. */
520
+ seek(target) {
521
+ if (disposed) throw new Error("PlaybackClock is disposed");
522
+ if (!Number.isFinite(target)) throw new Error(`seek target must be a finite number, got ${target}`);
523
+ frame = Math.max(0, Math.min(lastFrame, Math.round(target)));
524
+ if (playing) {
525
+ anchorTime = now();
526
+ anchorFrame = frame;
527
+ }
528
+ notify();
529
+ },
530
+ isPlaying() {
531
+ return playing;
532
+ },
533
+ getFrame() {
534
+ return frame;
535
+ },
536
+ subscribe(listener) {
537
+ listeners.add(listener);
538
+ return () => {
539
+ listeners.delete(listener);
540
+ };
541
+ },
542
+ dispose() {
543
+ playing = false;
544
+ stopLoop();
545
+ listeners.clear();
546
+ disposed = true;
547
+ }
548
+ };
549
+ }
550
+
551
+ // src/sequences-react/engine/snap.ts
552
+ function collectSnapPoints(timeline, playheadFrame) {
553
+ if (!Number.isInteger(playheadFrame) || playheadFrame < 0) {
554
+ throw new Error(`playheadFrame must be a non-negative integer, got ${playheadFrame}`);
555
+ }
556
+ const points = [];
557
+ for (const clip of timeline.clips) {
558
+ points.push({ frame: clip.startFrame, kind: "clip-start", clipId: clip.id });
559
+ points.push({ frame: clip.startFrame + clip.durationFrames, kind: "clip-end", clipId: clip.id });
560
+ }
561
+ points.push({ frame: playheadFrame, kind: "playhead" });
562
+ points.push({ frame: timeline.sequence.durationFrames, kind: "sequence-end" });
563
+ return points.sort((a, b) => a.frame - b.frame || a.kind.localeCompare(b.kind));
564
+ }
565
+ function applySnap(frame, points, opts) {
566
+ if (!Number.isFinite(frame)) throw new Error(`frame must be a finite number, got ${frame}`);
567
+ if (!Number.isFinite(opts.zoom) || opts.zoom <= 0) {
568
+ throw new Error(`zoom must be a positive finite number (pixels per frame), got ${opts.zoom}`);
569
+ }
570
+ const thresholdPx = opts.thresholdPx ?? 10;
571
+ if (!Number.isFinite(thresholdPx) || thresholdPx < 0) {
572
+ throw new Error(`thresholdPx must be a non-negative finite number, got ${thresholdPx}`);
573
+ }
574
+ const thresholdFrames = thresholdPx / opts.zoom;
575
+ let best = null;
576
+ let bestDistance = Infinity;
577
+ for (const point of points) {
578
+ if (opts.exclude && opts.exclude(point)) continue;
579
+ const distance = Math.abs(point.frame - frame);
580
+ if (distance < bestDistance) {
581
+ best = point;
582
+ bestDistance = distance;
583
+ }
584
+ }
585
+ if (best !== null && bestDistance <= thresholdFrames) {
586
+ return { frame: best.frame, snapped: true, point: best };
587
+ }
588
+ return { frame, snapped: false, point: null };
589
+ }
590
+
591
+ // src/sequences-react/engine/zoom.ts
592
+ function createZoomMath(config) {
593
+ const { minZoom, maxZoom } = config;
594
+ if (!Number.isFinite(minZoom) || minZoom <= 0) {
595
+ throw new Error(`minZoom must be a positive finite number, got ${minZoom}`);
596
+ }
597
+ if (!Number.isFinite(maxZoom) || maxZoom <= minZoom) {
598
+ throw new Error(`maxZoom must be finite and greater than minZoom ${minZoom}, got ${maxZoom}`);
599
+ }
600
+ const ratio = maxZoom / minZoom;
601
+ return {
602
+ minZoom,
603
+ maxZoom,
604
+ /** Slider clamps into [0, 1]: range inputs can overshoot during fast
605
+ * drags and the boundary value is always the right answer. */
606
+ sliderToZoom(slider) {
607
+ if (!Number.isFinite(slider)) throw new Error(`slider must be a finite number, got ${slider}`);
608
+ const t = Math.min(1, Math.max(0, slider));
609
+ return minZoom * Math.pow(ratio, t);
610
+ },
611
+ zoomToSlider(zoom) {
612
+ if (!Number.isFinite(zoom) || zoom <= 0) throw new Error(`zoom must be a positive finite number, got ${zoom}`);
613
+ const clamped = Math.min(maxZoom, Math.max(minZoom, zoom));
614
+ return Math.log(clamped / minZoom) / Math.log(ratio);
615
+ }
616
+ };
617
+ }
618
+ function assertViewport(view) {
619
+ if (!Number.isFinite(view.zoom) || view.zoom <= 0) {
620
+ throw new Error(`viewport zoom must be a positive finite number, got ${view.zoom}`);
621
+ }
622
+ if (!Number.isFinite(view.scrollLeft)) {
623
+ throw new Error(`viewport scrollLeft must be a finite number, got ${view.scrollLeft}`);
624
+ }
625
+ }
626
+ function frameToPixel(frame, view) {
627
+ assertViewport(view);
628
+ if (!Number.isFinite(frame)) throw new Error(`frame must be a finite number, got ${frame}`);
629
+ return frame * view.zoom - view.scrollLeft;
630
+ }
631
+ function pixelToFrame(pixel, view) {
632
+ assertViewport(view);
633
+ if (!Number.isFinite(pixel)) throw new Error(`pixel must be a finite number, got ${pixel}`);
634
+ return Math.max(0, Math.round((pixel + view.scrollLeft) / view.zoom));
635
+ }
636
+ function snapPixel(value, devicePixelRatio) {
637
+ if (!Number.isFinite(value)) throw new Error(`value must be a finite number, got ${value}`);
638
+ if (!Number.isFinite(devicePixelRatio) || devicePixelRatio <= 0) {
639
+ throw new Error(`devicePixelRatio must be a positive finite number, got ${devicePixelRatio}`);
640
+ }
641
+ return Math.round(value * devicePixelRatio) / devicePixelRatio;
642
+ }
643
+
644
+ // src/sequences-react/media/frame-provider.ts
645
+ var DEFAULT_MAX_MEDIA_ELEMENTS = 4;
646
+ var SEEK_TOLERANCE_SECONDS = 1 / 60;
647
+ var SEEK_TIMEOUT_MS = 5e3;
648
+ function containFitRect(source, dest) {
649
+ if (!(source.width > 0) || !(source.height > 0)) {
650
+ throw new Error(`containFitRect requires positive source dimensions, got ${source.width}x${source.height}`);
651
+ }
652
+ if (!(dest.width > 0) || !(dest.height > 0)) {
653
+ throw new Error(`containFitRect requires positive destination dimensions, got ${dest.width}x${dest.height}`);
654
+ }
655
+ const scale = Math.min(dest.width / source.width, dest.height / source.height);
656
+ const width = source.width * scale;
657
+ const height = source.height * scale;
658
+ return {
659
+ x: dest.x + (dest.width - width) / 2,
660
+ y: dest.y + (dest.height - height) / 2,
661
+ width,
662
+ height
663
+ };
664
+ }
665
+ function needsSeek(currentTimeSeconds, targetSeconds) {
666
+ return Math.abs(currentTimeSeconds - targetSeconds) >= SEEK_TOLERANCE_SECONDS;
667
+ }
668
+ function createMediaElementPool(opts) {
669
+ if (!Number.isInteger(opts.maxElements) || opts.maxElements < 1) {
670
+ throw new Error(`maxElements must be a positive integer, got ${opts.maxElements}`);
671
+ }
672
+ const entries = /* @__PURE__ */ new Map();
673
+ let disposed = false;
674
+ const evictOverBudget = () => {
675
+ if (entries.size <= opts.maxElements) return;
676
+ for (const [url, entry] of entries) {
677
+ if (entries.size <= opts.maxElements) return;
678
+ if (entry.pinned > 0) continue;
679
+ entries.delete(url);
680
+ opts.destroy(entry.element, url);
681
+ }
682
+ };
683
+ return {
684
+ acquire(url) {
685
+ if (disposed) throw new Error(`media element pool is disposed \u2014 cannot acquire ${url}`);
686
+ let entry = entries.get(url);
687
+ if (entry) {
688
+ entries.delete(url);
689
+ } else {
690
+ entry = { element: opts.create(url), pinned: 0 };
691
+ }
692
+ entries.set(url, entry);
693
+ entry.pinned += 1;
694
+ evictOverBudget();
695
+ let released = false;
696
+ return {
697
+ element: entry.element,
698
+ release: () => {
699
+ if (released) return;
700
+ released = true;
701
+ entry.pinned -= 1;
702
+ evictOverBudget();
703
+ }
704
+ };
705
+ },
706
+ has: (url) => entries.has(url),
707
+ size: () => entries.size,
708
+ dispose() {
709
+ disposed = true;
710
+ for (const [url, entry] of entries) opts.destroy(entry.element, url);
711
+ entries.clear();
712
+ }
713
+ };
714
+ }
715
+ var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set(["apng", "avif", "bmp", "gif", "jpeg", "jpg", "png", "svg", "webp"]);
716
+ var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set(["m4v", "mkv", "mov", "mp4", "mpeg", "mpg", "ogv", "webm"]);
717
+ function classifyMediaUrl(url) {
718
+ const match = /\.([a-z0-9]+)(?:[?#].*)?$/i.exec(url);
719
+ const extension = match?.[1]?.toLowerCase();
720
+ if (extension === void 0) return "unknown";
721
+ if (VIDEO_EXTENSIONS.has(extension)) return "video";
722
+ if (IMAGE_EXTENSIONS.has(extension)) return "image";
723
+ return "unknown";
724
+ }
725
+ async function probeMediaKind(url) {
726
+ const known = classifyMediaUrl(url);
727
+ if (known !== "unknown") return known;
728
+ let contentType = null;
729
+ try {
730
+ const response = await fetch(url, { method: "HEAD" });
731
+ contentType = response.headers.get("content-type");
732
+ } catch {
733
+ return "image";
734
+ }
735
+ if (contentType !== null) {
736
+ if (contentType.startsWith("video/")) return "video";
737
+ if (contentType.startsWith("audio/")) {
738
+ throw new Error(`cannot draw frames from audio media ${url} (content-type ${contentType})`);
739
+ }
740
+ }
741
+ return "image";
742
+ }
743
+ function requireDocument(caller) {
744
+ if (typeof document === "undefined") {
745
+ throw new Error(`${caller} requires a browser document \u2014 frame providers are client-side only`);
746
+ }
747
+ return document;
748
+ }
749
+ function assertSourceSeconds(sourceSeconds) {
750
+ if (!Number.isFinite(sourceSeconds) || sourceSeconds < 0) {
751
+ throw new Error(`sourceSeconds must be a non-negative finite number, got ${sourceSeconds}`);
752
+ }
753
+ }
754
+ function createPooledVideo(url) {
755
+ const video = requireDocument("createVideoElementFrameProvider").createElement("video");
756
+ video.crossOrigin = "anonymous";
757
+ video.muted = true;
758
+ video.preload = "auto";
759
+ video.playsInline = true;
760
+ video.src = url;
761
+ return video;
762
+ }
763
+ function destroyPooledVideo(video) {
764
+ video.pause();
765
+ video.removeAttribute("src");
766
+ video.load();
767
+ }
768
+ function awaitMediaEvent(video, eventName, url) {
769
+ return new Promise((resolve, reject) => {
770
+ const cleanup = () => {
771
+ clearTimeout(timer);
772
+ video.removeEventListener(eventName, onSuccess);
773
+ video.removeEventListener("error", onError);
774
+ };
775
+ const onSuccess = () => {
776
+ cleanup();
777
+ resolve();
778
+ };
779
+ const onError = () => {
780
+ cleanup();
781
+ const detail = video.error ? `code ${video.error.code}: ${video.error.message}` : "no MediaError attached";
782
+ reject(new Error(`media error while waiting for '${eventName}' on ${url} (${detail})`));
783
+ };
784
+ const timer = setTimeout(() => {
785
+ cleanup();
786
+ reject(new Error(`timed out after ${SEEK_TIMEOUT_MS}ms waiting for '${eventName}' on ${url}`));
787
+ }, SEEK_TIMEOUT_MS);
788
+ video.addEventListener(eventName, onSuccess);
789
+ video.addEventListener("error", onError);
790
+ });
791
+ }
792
+ async function seekVideo(video, targetSeconds, url) {
793
+ const seeked = awaitMediaEvent(video, "seeked", url);
794
+ video.currentTime = Number.isFinite(video.duration) && video.duration > 0 ? Math.min(targetSeconds, video.duration) : targetSeconds;
795
+ await seeked;
796
+ }
797
+ async function drawVideoFrame(video, url, sourceSeconds, ctx, rect) {
798
+ if (video.readyState < 1) await awaitMediaEvent(video, "loadedmetadata", url);
799
+ if (needsSeek(video.currentTime, sourceSeconds)) await seekVideo(video, sourceSeconds, url);
800
+ if (video.videoWidth === 0 || video.videoHeight === 0) {
801
+ throw new Error(`media at ${url} decoded with no video frames (audio-only or corrupt) \u2014 cannot draw`);
802
+ }
803
+ const fit = containFitRect({ width: video.videoWidth, height: video.videoHeight }, rect);
804
+ ctx.drawImage(video, fit.x, fit.y, fit.width, fit.height);
805
+ }
806
+ function createPooledImage(url) {
807
+ const element = requireDocument("createImageFrameProvider").createElement("img");
808
+ element.crossOrigin = "anonymous";
809
+ element.src = url;
810
+ const ready = element.decode().then(
811
+ () => void 0,
812
+ (error) => {
813
+ throw new Error(`failed to decode image ${url}`, { cause: error });
814
+ }
815
+ );
816
+ void ready.catch(() => void 0);
817
+ return { element, ready };
818
+ }
819
+ function createImageFrameProvider(opts) {
820
+ const pool = createMediaElementPool({
821
+ maxElements: opts?.maxElements ?? DEFAULT_MAX_MEDIA_ELEMENTS,
822
+ create: createPooledImage,
823
+ destroy: (pooled) => {
824
+ pooled.element.src = "";
825
+ }
826
+ });
827
+ return {
828
+ async drawFrame(mediaUrl, sourceSeconds, ctx, rect) {
829
+ assertSourceSeconds(sourceSeconds);
830
+ const lease = pool.acquire(mediaUrl);
831
+ try {
832
+ await lease.element.ready;
833
+ const image = lease.element.element;
834
+ const fit = containFitRect({ width: image.naturalWidth, height: image.naturalHeight }, rect);
835
+ ctx.drawImage(image, fit.x, fit.y, fit.width, fit.height);
836
+ } finally {
837
+ lease.release();
838
+ }
839
+ },
840
+ prefetch(mediaUrl) {
841
+ pool.acquire(mediaUrl).release();
842
+ },
843
+ dispose() {
844
+ pool.dispose();
845
+ }
846
+ };
847
+ }
848
+ function createVideoElementFrameProvider(opts) {
849
+ const maxElements = opts?.maxElements ?? DEFAULT_MAX_MEDIA_ELEMENTS;
850
+ const videoPool = createMediaElementPool({
851
+ maxElements,
852
+ create: createPooledVideo,
853
+ destroy: destroyPooledVideo
854
+ });
855
+ const imageProvider = createImageFrameProvider({ maxElements });
856
+ const kindByUrl = /* @__PURE__ */ new Map();
857
+ const drawQueue = /* @__PURE__ */ new Map();
858
+ const resolveKind = (url) => {
859
+ let pending = kindByUrl.get(url);
860
+ if (pending === void 0) {
861
+ pending = probeMediaKind(url);
862
+ pending.catch(() => kindByUrl.delete(url));
863
+ kindByUrl.set(url, pending);
864
+ }
865
+ return pending;
866
+ };
867
+ const enqueueVideoDraw = (url, work) => {
868
+ const previous = drawQueue.get(url) ?? Promise.resolve();
869
+ const run = previous.then(work, work);
870
+ const tail = run.then(
871
+ () => void 0,
872
+ () => void 0
873
+ ).then(() => {
874
+ if (drawQueue.get(url) === tail) drawQueue.delete(url);
875
+ });
876
+ drawQueue.set(url, tail);
877
+ return run;
878
+ };
879
+ return {
880
+ async drawFrame(mediaUrl, sourceSeconds, ctx, rect) {
881
+ assertSourceSeconds(sourceSeconds);
882
+ const kind = await resolveKind(mediaUrl);
883
+ if (kind === "image") {
884
+ await imageProvider.drawFrame(mediaUrl, sourceSeconds, ctx, rect);
885
+ return;
886
+ }
887
+ await enqueueVideoDraw(mediaUrl, async () => {
888
+ const lease = videoPool.acquire(mediaUrl);
889
+ try {
890
+ await drawVideoFrame(lease.element, mediaUrl, sourceSeconds, ctx, rect);
891
+ } finally {
892
+ lease.release();
893
+ }
894
+ });
895
+ },
896
+ prefetch(mediaUrl) {
897
+ void resolveKind(mediaUrl).then((kind) => {
898
+ if (kind === "image") {
899
+ imageProvider.prefetch(mediaUrl);
900
+ return;
901
+ }
902
+ videoPool.acquire(mediaUrl).release();
903
+ }).catch(() => void 0);
904
+ },
905
+ dispose() {
906
+ videoPool.dispose();
907
+ imageProvider.dispose();
908
+ kindByUrl.clear();
909
+ drawQueue.clear();
910
+ }
911
+ };
912
+ }
913
+
914
+ // src/sequences-react/components/composite-command.ts
915
+ function compositeCommand(label, commands) {
916
+ if (commands.length === 0) throw new Error("compositeCommand requires at least one command");
917
+ return {
918
+ label,
919
+ execute: (state) => commands.reduce((acc, command) => command.execute(acc), state),
920
+ undo: (state) => [...commands].reverse().reduce((acc, command) => command.undo(acc), state),
921
+ operations: () => commands.flatMap((command) => command.operations()),
922
+ inverseOperations: () => [...commands].reverse().flatMap((command) => command.inverseOperations())
923
+ };
924
+ }
925
+
926
+ // src/sequences-react/components/interaction-math.ts
927
+ function framesFromPixelDelta(deltaX, zoom) {
928
+ if (!Number.isFinite(deltaX)) throw new Error("deltaX must be a finite pixel delta");
929
+ if (!Number.isFinite(zoom) || zoom <= 0) throw new Error("zoom (pixels per frame) must be a positive finite number");
930
+ return Math.round(deltaX / zoom);
931
+ }
932
+ function moveDragStartFrame(input) {
933
+ return clampClipStart({
934
+ startFrame: input.originStartFrame + input.deltaFrames,
935
+ durationFrames: input.durationFrames,
936
+ sequenceDurationFrames: input.sequenceDurationFrames
937
+ });
938
+ }
939
+ function trimStartDrag(input) {
940
+ const endFrame = input.originStartFrame + input.originDurationFrames;
941
+ const minStart = Math.max(0, input.originStartFrame - input.originSourceInFrame);
942
+ const maxStart = endFrame - MIN_SEQUENCE_CLIP_FRAMES;
943
+ const startFrame = Math.max(minStart, Math.min(maxStart, input.originStartFrame + input.deltaFrames));
944
+ return {
945
+ startFrame,
946
+ durationFrames: endFrame - startFrame,
947
+ sourceInFrame: input.originSourceInFrame + (startFrame - input.originStartFrame)
948
+ };
949
+ }
950
+ function trimEndDrag(input) {
951
+ const bySequence = input.sequenceDurationFrames - input.originStartFrame;
952
+ const bySource = input.sourceDurationFrames === void 0 ? Number.POSITIVE_INFINITY : input.sourceDurationFrames - input.sourceInFrame;
953
+ const maxDuration = Math.min(bySequence, bySource);
954
+ const durationFrames = Math.max(
955
+ MIN_SEQUENCE_CLIP_FRAMES,
956
+ Math.min(maxDuration, input.originDurationFrames + input.deltaFrames)
957
+ );
958
+ return { durationFrames };
959
+ }
960
+ var TICK_STEPS_SECONDS = [1, 5, 10, 30, 60, 300];
961
+ function selectTickStepSeconds(input) {
962
+ if (!Number.isFinite(input.zoom) || input.zoom <= 0) throw new Error("zoom must be a positive finite number");
963
+ if (!Number.isInteger(input.fps) || input.fps <= 0) throw new Error("fps must be a positive integer");
964
+ const minSpacing = input.minSpacingPx ?? 80;
965
+ for (const step of TICK_STEPS_SECONDS) {
966
+ if (step * input.fps * input.zoom >= minSpacing) return step;
967
+ }
968
+ const pxPerMinute = 60 * input.fps * input.zoom;
969
+ return Math.ceil(minSpacing / pxPerMinute) * 60;
970
+ }
971
+ function letterboxRect(input) {
972
+ const { containerWidth, containerHeight, mediaWidth, mediaHeight } = input;
973
+ if (containerWidth <= 0 || containerHeight <= 0) throw new Error("container dimensions must be positive");
974
+ if (mediaWidth <= 0 || mediaHeight <= 0) throw new Error("media dimensions must be positive");
975
+ const scale = Math.min(containerWidth / mediaWidth, containerHeight / mediaHeight);
976
+ const width = mediaWidth * scale;
977
+ const height = mediaHeight * scale;
978
+ return {
979
+ x: (containerWidth - width) / 2,
980
+ y: (containerHeight - height) / 2,
981
+ width,
982
+ height
983
+ };
984
+ }
985
+ function captionFontPx(canvasCssHeight) {
986
+ if (!Number.isFinite(canvasCssHeight) || canvasCssHeight <= 0) throw new Error("canvas height must be positive");
987
+ return Math.max(12, Math.round(canvasCssHeight / 18));
988
+ }
989
+ function clipChipGeometry(input) {
990
+ return {
991
+ left: input.startFrame * input.zoom,
992
+ width: Math.max(2, input.durationFrames * input.zoom)
993
+ };
994
+ }
995
+ function chooseMoveSnap(input) {
996
+ const startDelta = input.startSnap.snapped ? Math.abs(input.startSnap.frame - input.candidateStartFrame) : Number.POSITIVE_INFINITY;
997
+ const endStartFrame = input.endSnap.frame - input.durationFrames;
998
+ const endDelta = input.endSnap.snapped ? Math.abs(endStartFrame - input.candidateStartFrame) : Number.POSITIVE_INFINITY;
999
+ if (startDelta === Number.POSITIVE_INFINITY && endDelta === Number.POSITIVE_INFINITY) {
1000
+ return { startFrame: input.candidateStartFrame, point: null };
1001
+ }
1002
+ if (startDelta <= endDelta) return { startFrame: input.startSnap.frame, point: input.startSnap.point };
1003
+ return { startFrame: endStartFrame, point: input.endSnap.point };
1004
+ }
1005
+
1006
+ // src/sequences-react/components/PreviewCanvas.tsx
1007
+ import { useEffect, useMemo, useRef, useState } from "react";
1008
+ import { jsx, jsxs } from "react/jsx-runtime";
1009
+ function PreviewCanvas({ timeline, clock, frameProvider, className }) {
1010
+ const containerRef = useRef(null);
1011
+ const canvasRef = useRef(null);
1012
+ const [size, setSize] = useState(null);
1013
+ const [drawError, setDrawError] = useState(null);
1014
+ const paintQueueRef = useRef({ running: false, queuedFrame: null });
1015
+ const paintInputsRef = useRef({ timeline, frameProvider, size });
1016
+ paintInputsRef.current = { timeline, frameProvider, size };
1017
+ useEffect(() => {
1018
+ const container = containerRef.current;
1019
+ if (!container) return;
1020
+ function measure() {
1021
+ const node = containerRef.current;
1022
+ if (!node) return;
1023
+ const rect = node.getBoundingClientRect();
1024
+ if (rect.width <= 0 || rect.height <= 0) return;
1025
+ const fit = letterboxRect({
1026
+ containerWidth: rect.width,
1027
+ containerHeight: rect.height,
1028
+ mediaWidth: timeline.sequence.width,
1029
+ mediaHeight: timeline.sequence.height
1030
+ });
1031
+ setSize((current) => {
1032
+ const next = { width: Math.round(fit.width), height: Math.round(fit.height) };
1033
+ return current && current.width === next.width && current.height === next.height ? current : next;
1034
+ });
1035
+ }
1036
+ measure();
1037
+ if (typeof ResizeObserver === "undefined") return;
1038
+ const observer = new ResizeObserver(measure);
1039
+ observer.observe(container);
1040
+ return () => observer.disconnect();
1041
+ }, [timeline.sequence.width, timeline.sequence.height]);
1042
+ const requestPaint = useMemo(() => {
1043
+ async function paint(frame) {
1044
+ const { timeline: current, frameProvider: provider, size: cssSize } = paintInputsRef.current;
1045
+ const canvas = canvasRef.current;
1046
+ if (!canvas || !cssSize) return;
1047
+ const ctx = canvas.getContext("2d");
1048
+ if (!ctx) return;
1049
+ const dpr = typeof window !== "undefined" && window.devicePixelRatio || 1;
1050
+ const backingWidth = Math.round(cssSize.width * dpr);
1051
+ const backingHeight = Math.round(cssSize.height * dpr);
1052
+ if (canvas.width !== backingWidth) canvas.width = backingWidth;
1053
+ if (canvas.height !== backingHeight) canvas.height = backingHeight;
1054
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
1055
+ ctx.fillStyle = "#000";
1056
+ ctx.fillRect(0, 0, cssSize.width, cssSize.height);
1057
+ const paintFrame = Math.max(0, Math.min(frame, current.sequence.durationFrames - 1));
1058
+ const snapshot = snapshotFrame(current, paintFrame);
1059
+ const mediaEntries = snapshot.active.filter(({ track, clip }) => track.kind === "video" && !track.muted && clip.media !== void 0 && (clip.media.kind === "video" || clip.media.kind === "image"));
1060
+ const top = mediaEntries[mediaEntries.length - 1];
1061
+ if (top && top.clip.media) {
1062
+ const sourceSeconds = framesToSeconds(top.clip.sourceInFrame + (paintFrame - top.clip.startFrame), current.sequence.fps);
1063
+ await provider.drawFrame(top.clip.media.url, sourceSeconds, ctx, {
1064
+ x: 0,
1065
+ y: 0,
1066
+ width: cssSize.width,
1067
+ height: cssSize.height
1068
+ });
1069
+ }
1070
+ if (snapshot.captions.length > 0) {
1071
+ const fontPx = captionFontPx(cssSize.height);
1072
+ ctx.font = `600 ${fontPx}px system-ui, sans-serif`;
1073
+ ctx.textAlign = "center";
1074
+ ctx.textBaseline = "middle";
1075
+ const barHeight = fontPx * 1.6;
1076
+ let centerY = cssSize.height - barHeight;
1077
+ for (const caption of [...snapshot.captions].reverse()) {
1078
+ const textWidth = Math.min(ctx.measureText(caption.text).width, cssSize.width * 0.86);
1079
+ const barWidth = textWidth + fontPx * 1.2;
1080
+ ctx.fillStyle = "rgba(0, 0, 0, 0.8)";
1081
+ ctx.fillRect((cssSize.width - barWidth) / 2, centerY - barHeight / 2, barWidth, barHeight);
1082
+ ctx.fillStyle = "#fff";
1083
+ ctx.fillText(caption.text, cssSize.width / 2, centerY, cssSize.width * 0.86);
1084
+ centerY -= barHeight + fontPx * 0.25;
1085
+ }
1086
+ }
1087
+ }
1088
+ return function requestPaint2(frame) {
1089
+ const queue = paintQueueRef.current;
1090
+ if (queue.running) {
1091
+ queue.queuedFrame = frame;
1092
+ return;
1093
+ }
1094
+ queue.running = true;
1095
+ void (async () => {
1096
+ let next = frame;
1097
+ while (next !== null) {
1098
+ const target = next;
1099
+ queue.queuedFrame = null;
1100
+ try {
1101
+ await paint(target);
1102
+ setDrawError(null);
1103
+ } catch (error) {
1104
+ setDrawError(error instanceof Error ? error.message : String(error));
1105
+ }
1106
+ next = queue.queuedFrame;
1107
+ }
1108
+ queue.running = false;
1109
+ })();
1110
+ };
1111
+ }, []);
1112
+ useEffect(() => {
1113
+ requestPaint(clock.getFrame());
1114
+ return clock.subscribe(requestPaint);
1115
+ }, [clock, requestPaint]);
1116
+ useEffect(() => {
1117
+ requestPaint(clock.getFrame());
1118
+ }, [timeline, size, clock, requestPaint]);
1119
+ return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: `relative flex min-h-0 flex-1 items-center justify-center overflow-hidden bg-black ${className ?? ""}`, children: [
1120
+ /* @__PURE__ */ jsx(
1121
+ "canvas",
1122
+ {
1123
+ ref: canvasRef,
1124
+ "data-preview-canvas": true,
1125
+ className: "block",
1126
+ style: size ? { width: `${size.width}px`, height: `${size.height}px` } : { width: "100%", height: "100%" }
1127
+ }
1128
+ ),
1129
+ drawError ? /* @__PURE__ */ jsx("p", { className: "absolute inset-x-3 bottom-2 truncate rounded bg-rose-950/80 px-2 py-1 text-center text-xs text-rose-200", role: "alert", children: drawError }) : null
1130
+ ] });
1131
+ }
1132
+
1133
+ // src/sequences-react/components/SnapIndicatorLine.tsx
1134
+ import { jsx as jsx2 } from "react/jsx-runtime";
1135
+ function SnapIndicatorLine({ point, zoom }) {
1136
+ if (!point) return null;
1137
+ return /* @__PURE__ */ jsx2(
1138
+ "div",
1139
+ {
1140
+ "data-snap-kind": point.kind,
1141
+ className: "pointer-events-none absolute bottom-0 top-0 z-30 w-px bg-[var(--brand-primary)] shadow-[0_0_8px_var(--brand-primary)]",
1142
+ style: { left: `${point.frame * zoom}px` }
1143
+ }
1144
+ );
1145
+ }
1146
+
1147
+ // src/sequences-react/components/TimelinePlayhead.tsx
1148
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1149
+ function TimelinePlayhead({ frame, zoom }) {
1150
+ return /* @__PURE__ */ jsxs2(
1151
+ "div",
1152
+ {
1153
+ "data-timeline-playhead": true,
1154
+ className: "pointer-events-none absolute bottom-0 top-0 z-20",
1155
+ style: { left: `${frame * zoom}px` },
1156
+ children: [
1157
+ /* @__PURE__ */ jsx3("div", { className: "absolute bottom-0 top-0 w-px bg-[var(--brand-primary)] shadow-[0_0_10px_var(--brand-primary)]" }),
1158
+ /* @__PURE__ */ jsx3(
1159
+ "div",
1160
+ {
1161
+ className: "absolute -left-[5px] top-0 h-0 w-0 border-x-[5px] border-t-[7px] border-x-transparent",
1162
+ style: { borderTopColor: "var(--brand-primary)" }
1163
+ }
1164
+ )
1165
+ ]
1166
+ }
1167
+ );
1168
+ }
1169
+
1170
+ // src/sequences-react/components/TimelineRuler.tsx
1171
+ import { useMemo as useMemo2 } from "react";
1172
+ import { jsx as jsx4 } from "react/jsx-runtime";
1173
+ function TimelineRuler({ fps, durationFrames, zoom, onScrub }) {
1174
+ const ticks = useMemo2(() => {
1175
+ const stepSeconds = selectTickStepSeconds({ zoom, fps });
1176
+ const majorStepFrames = stepSeconds * fps;
1177
+ const minorStepFrames = Math.round(majorStepFrames / 5);
1178
+ const drawMinor = minorStepFrames * zoom >= 8 && minorStepFrames >= 1;
1179
+ const result = [];
1180
+ for (let frame = 0; frame <= durationFrames; frame += majorStepFrames) {
1181
+ result.push({ frame, label: formatTimecode(frame, fps) });
1182
+ if (!drawMinor) continue;
1183
+ for (let minor = 1; minor < 5; minor += 1) {
1184
+ const minorFrame = frame + minor * minorStepFrames;
1185
+ if (minorFrame >= durationFrames) break;
1186
+ result.push({ frame: minorFrame, label: null });
1187
+ }
1188
+ }
1189
+ return result;
1190
+ }, [durationFrames, fps, zoom]);
1191
+ function frameFromPointer(event) {
1192
+ const rect = event.currentTarget.getBoundingClientRect();
1193
+ const frame = Math.round((event.clientX - rect.left) / zoom);
1194
+ return Math.max(0, Math.min(durationFrames, frame));
1195
+ }
1196
+ function handlePointerDown(event) {
1197
+ if (event.button !== 0) return;
1198
+ event.preventDefault();
1199
+ if (typeof event.currentTarget.setPointerCapture === "function") {
1200
+ event.currentTarget.setPointerCapture(event.pointerId);
1201
+ }
1202
+ onScrub(frameFromPointer(event));
1203
+ }
1204
+ function handlePointerMove(event) {
1205
+ if (typeof event.currentTarget.hasPointerCapture !== "function") return;
1206
+ if (!event.currentTarget.hasPointerCapture(event.pointerId)) return;
1207
+ onScrub(frameFromPointer(event));
1208
+ }
1209
+ return /* @__PURE__ */ jsx4(
1210
+ "div",
1211
+ {
1212
+ "data-timeline-ruler": true,
1213
+ className: "relative h-7 cursor-ew-resize select-none border-b border-[var(--border-default)] bg-[var(--bg-input)]",
1214
+ style: { width: `${durationFrames * zoom}px` },
1215
+ onPointerDown: handlePointerDown,
1216
+ onPointerMove: handlePointerMove,
1217
+ children: ticks.map((tick) => /* @__PURE__ */ jsx4(
1218
+ "div",
1219
+ {
1220
+ className: `absolute bottom-0 w-px bg-[var(--border-default)] ${tick.label !== null ? "top-2.5" : "top-[18px]"}`,
1221
+ style: { left: `${tick.frame * zoom}px` },
1222
+ children: tick.label !== null ? /* @__PURE__ */ jsx4("span", { className: "absolute -top-2 left-1 whitespace-nowrap font-mono text-[10px] leading-none text-[var(--text-muted)]", children: tick.label }) : null
1223
+ },
1224
+ tick.frame
1225
+ ))
1226
+ }
1227
+ );
1228
+ }
1229
+
1230
+ // src/sequences-react/components/TimelineClipChip.tsx
1231
+ import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
1232
+
1233
+ // src/sequences-react/media/waveform.ts
1234
+ function computeWaveform(buffer, bucketCount) {
1235
+ if (!Number.isInteger(bucketCount) || bucketCount < 1) {
1236
+ throw new Error(`bucketCount must be a positive integer, got ${bucketCount}`);
1237
+ }
1238
+ if (!Number.isInteger(buffer.length) || buffer.length < 1) {
1239
+ throw new Error(`audio buffer is empty (length ${buffer.length}) \u2014 cannot compute a waveform`);
1240
+ }
1241
+ if (!Number.isInteger(buffer.numberOfChannels) || buffer.numberOfChannels < 1) {
1242
+ throw new Error(`audio buffer must have at least one channel, got ${buffer.numberOfChannels}`);
1243
+ }
1244
+ if (!Number.isFinite(buffer.duration) || buffer.duration <= 0) {
1245
+ throw new Error(`audio buffer duration must be positive, got ${buffer.duration}`);
1246
+ }
1247
+ const samplesPerBucket = Math.ceil(buffer.length / bucketCount);
1248
+ const peaks = new Float32Array(bucketCount);
1249
+ for (let channel = 0; channel < buffer.numberOfChannels; channel++) {
1250
+ const data = buffer.getChannelData(channel);
1251
+ if (data.length !== buffer.length) {
1252
+ throw new Error(`channel ${channel} has ${data.length} samples, expected ${buffer.length}`);
1253
+ }
1254
+ for (let bucket = 0; bucket < bucketCount; bucket++) {
1255
+ const start = bucket * samplesPerBucket;
1256
+ if (start >= data.length) break;
1257
+ let peak = peaks[bucket];
1258
+ for (const sample of data.subarray(start, Math.min(data.length, start + samplesPerBucket))) {
1259
+ const magnitude = Math.abs(sample);
1260
+ if (magnitude > peak) peak = magnitude;
1261
+ }
1262
+ peaks[bucket] = peak;
1263
+ }
1264
+ }
1265
+ return { peaks, samplesPerBucket, durationSeconds: buffer.duration };
1266
+ }
1267
+ async function loadWaveform(mediaUrl, bucketCount, ctx) {
1268
+ const response = await fetch(mediaUrl);
1269
+ if (!response.ok) {
1270
+ throw new Error(`failed to fetch audio for waveform: ${response.status} ${response.statusText} from ${mediaUrl}`);
1271
+ }
1272
+ const bytes = await response.arrayBuffer();
1273
+ const ownsContext = ctx === void 0;
1274
+ if (ctx === void 0) {
1275
+ if (typeof AudioContext === "undefined") {
1276
+ throw new Error("loadWaveform requires Web Audio (AudioContext) \u2014 pass a ctx or call from a browser");
1277
+ }
1278
+ ctx = new AudioContext();
1279
+ }
1280
+ try {
1281
+ const decoded = await ctx.decodeAudioData(bytes);
1282
+ return computeWaveform(decoded, bucketCount);
1283
+ } finally {
1284
+ if (ownsContext) await ctx.close();
1285
+ }
1286
+ }
1287
+ function drawWaveform(ctx, data, rect, color) {
1288
+ if (data.peaks.length === 0) {
1289
+ throw new Error("waveform has no peaks \u2014 compute it with a positive bucketCount");
1290
+ }
1291
+ if (!(rect.width > 0) || !(rect.height > 0)) {
1292
+ throw new Error(`drawWaveform requires positive rect dimensions, got ${rect.width}x${rect.height}`);
1293
+ }
1294
+ ctx.fillStyle = color;
1295
+ const midline = rect.y + rect.height / 2;
1296
+ const step = rect.width / data.peaks.length;
1297
+ const barWidth = Math.max(1, step - 1);
1298
+ for (const [index, peak] of data.peaks.entries()) {
1299
+ const half = Math.min(1, Math.max(0, peak)) * (rect.height / 2);
1300
+ const barHeight = Math.max(1, half * 2);
1301
+ ctx.fillRect(rect.x + index * step, midline - barHeight / 2, barWidth, barHeight);
1302
+ }
1303
+ }
1304
+
1305
+ // src/sequences-react/components/TimelineClipChip.tsx
1306
+ import { Fragment, jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
1307
+ var KIND_TONES = {
1308
+ video: "border-sky-400/40 bg-sky-500/15",
1309
+ audio: "border-emerald-400/40 bg-emerald-500/15",
1310
+ caption: "border-amber-400/40 bg-amber-500/15",
1311
+ reference: "border-zinc-400/40 bg-zinc-500/15",
1312
+ agent: "border-violet-400/40 bg-violet-500/15"
1313
+ };
1314
+ var waveformCache = /* @__PURE__ */ new Map();
1315
+ var WAVEFORM_BUCKETS = 256;
1316
+ function sourceDurationFrames(clip, fps) {
1317
+ if (clip.sourceOutFrame !== null && clip.sourceOutFrame !== void 0) return clip.sourceOutFrame;
1318
+ if (clip.media?.durationSeconds !== void 0) return Math.round(clip.media.durationSeconds * fps);
1319
+ return void 0;
1320
+ }
1321
+ function TimelineClipChip(props) {
1322
+ const { clip, track, fps, zoom, selected, canWrite, frameProvider } = props;
1323
+ const rootRef = useRef2(null);
1324
+ const gestureRef = useRef2(null);
1325
+ const [preview, setPreview] = useState2(null);
1326
+ const previewRef = useRef2(null);
1327
+ const [editingText, setEditingText] = useState2(null);
1328
+ const posterRef = useRef2(null);
1329
+ const waveformRef = useRef2(null);
1330
+ const shown = preview ?? {
1331
+ startFrame: clip.startFrame,
1332
+ durationFrames: clip.durationFrames,
1333
+ sourceInFrame: clip.sourceInFrame,
1334
+ trackId: clip.trackId,
1335
+ translateY: 0,
1336
+ moved: false
1337
+ };
1338
+ const geometry = clipChipGeometry({ startFrame: shown.startFrame, durationFrames: shown.durationFrames, zoom });
1339
+ const interactive = canWrite && !track.locked;
1340
+ function collectLaneTargets() {
1341
+ const root = rootRef.current?.closest("[data-timeline-tracks]");
1342
+ const originLane = rootRef.current?.closest("[data-lane-track]");
1343
+ if (!root || !originLane) return [];
1344
+ const originTop = originLane.getBoundingClientRect().top;
1345
+ const targets = [];
1346
+ for (const lane of Array.from(root.querySelectorAll("[data-lane-track]"))) {
1347
+ if (lane.dataset.laneKind !== track.kind || lane.dataset.laneLocked === "true") continue;
1348
+ const rect = lane.getBoundingClientRect();
1349
+ const trackId = lane.dataset.laneTrack;
1350
+ if (!trackId) continue;
1351
+ targets.push({ trackId, top: rect.top, bottom: rect.bottom, offsetY: rect.top - originTop });
1352
+ }
1353
+ return targets;
1354
+ }
1355
+ function beginGesture(event, kind) {
1356
+ if (!interactive || event.button !== 0 || editingText !== null) return;
1357
+ event.preventDefault();
1358
+ event.stopPropagation();
1359
+ if (typeof event.currentTarget.setPointerCapture === "function") {
1360
+ event.currentTarget.setPointerCapture(event.pointerId);
1361
+ }
1362
+ gestureRef.current = {
1363
+ kind,
1364
+ pointerId: event.pointerId,
1365
+ originClientX: event.clientX,
1366
+ origin: { startFrame: clip.startFrame, durationFrames: clip.durationFrames, sourceInFrame: clip.sourceInFrame },
1367
+ laneTargets: kind === "move" ? collectLaneTargets() : [],
1368
+ originTrackId: clip.trackId
1369
+ };
1370
+ document.body.style.cursor = kind === "move" ? "grabbing" : "ew-resize";
1371
+ document.body.style.userSelect = "none";
1372
+ }
1373
+ function applyPreview(next) {
1374
+ previewRef.current = next;
1375
+ setPreview(next);
1376
+ }
1377
+ function updateGesture(event) {
1378
+ const gesture = gestureRef.current;
1379
+ if (!gesture || event.pointerId !== gesture.pointerId) return;
1380
+ const deltaFrames = framesFromPixelDelta(event.clientX - gesture.originClientX, zoom);
1381
+ const moved = previewRef.current?.moved || Math.abs(event.clientX - gesture.originClientX) > 3;
1382
+ if (gesture.kind === "move") {
1383
+ const candidate = moveDragStartFrame({
1384
+ originStartFrame: gesture.origin.startFrame,
1385
+ durationFrames: gesture.origin.durationFrames,
1386
+ deltaFrames,
1387
+ sequenceDurationFrames: props.sequenceDurationFrames
1388
+ });
1389
+ const snapped2 = props.snapMove({ startFrame: candidate, durationFrames: gesture.origin.durationFrames, clipId: clip.id });
1390
+ const startFrame = moveDragStartFrame({
1391
+ originStartFrame: snapped2.startFrame,
1392
+ durationFrames: gesture.origin.durationFrames,
1393
+ deltaFrames: 0,
1394
+ sequenceDurationFrames: props.sequenceDurationFrames
1395
+ });
1396
+ props.onSnapPointChange(startFrame === snapped2.startFrame ? snapped2.point : null);
1397
+ const lane = gesture.laneTargets.find((target) => event.clientY >= target.top && event.clientY < target.bottom);
1398
+ applyPreview({
1399
+ startFrame,
1400
+ durationFrames: gesture.origin.durationFrames,
1401
+ sourceInFrame: gesture.origin.sourceInFrame,
1402
+ trackId: lane?.trackId ?? gesture.originTrackId,
1403
+ translateY: lane?.offsetY ?? 0,
1404
+ moved
1405
+ });
1406
+ return;
1407
+ }
1408
+ if (gesture.kind === "trim-start") {
1409
+ const raw = trimStartDrag({
1410
+ originStartFrame: gesture.origin.startFrame,
1411
+ originDurationFrames: gesture.origin.durationFrames,
1412
+ originSourceInFrame: gesture.origin.sourceInFrame,
1413
+ deltaFrames
1414
+ });
1415
+ const snapped2 = props.snapEdge({ frame: raw.startFrame, clipId: clip.id });
1416
+ const clamped2 = trimStartDrag({
1417
+ originStartFrame: gesture.origin.startFrame,
1418
+ originDurationFrames: gesture.origin.durationFrames,
1419
+ originSourceInFrame: gesture.origin.sourceInFrame,
1420
+ deltaFrames: snapped2.frame - gesture.origin.startFrame
1421
+ });
1422
+ props.onSnapPointChange(clamped2.startFrame === snapped2.frame ? snapped2.point : null);
1423
+ applyPreview({ ...clamped2, trackId: gesture.originTrackId, translateY: 0, moved });
1424
+ return;
1425
+ }
1426
+ const rawEnd = gesture.origin.startFrame + trimEndDrag({
1427
+ originStartFrame: gesture.origin.startFrame,
1428
+ originDurationFrames: gesture.origin.durationFrames,
1429
+ sourceInFrame: gesture.origin.sourceInFrame,
1430
+ deltaFrames,
1431
+ sequenceDurationFrames: props.sequenceDurationFrames,
1432
+ sourceDurationFrames: sourceDurationFrames(clip, fps)
1433
+ }).durationFrames;
1434
+ const snapped = props.snapEdge({ frame: rawEnd, clipId: clip.id });
1435
+ const clamped = trimEndDrag({
1436
+ originStartFrame: gesture.origin.startFrame,
1437
+ originDurationFrames: gesture.origin.durationFrames,
1438
+ sourceInFrame: gesture.origin.sourceInFrame,
1439
+ deltaFrames: snapped.frame - (gesture.origin.startFrame + gesture.origin.durationFrames),
1440
+ sequenceDurationFrames: props.sequenceDurationFrames,
1441
+ sourceDurationFrames: sourceDurationFrames(clip, fps)
1442
+ });
1443
+ props.onSnapPointChange(gesture.origin.startFrame + clamped.durationFrames === snapped.frame ? snapped.point : null);
1444
+ applyPreview({
1445
+ startFrame: gesture.origin.startFrame,
1446
+ durationFrames: clamped.durationFrames,
1447
+ sourceInFrame: gesture.origin.sourceInFrame,
1448
+ trackId: gesture.originTrackId,
1449
+ translateY: 0,
1450
+ moved
1451
+ });
1452
+ }
1453
+ function endGestureCleanup() {
1454
+ gestureRef.current = null;
1455
+ previewRef.current = null;
1456
+ setPreview(null);
1457
+ props.onSnapPointChange(null);
1458
+ document.body.style.cursor = "";
1459
+ document.body.style.userSelect = "";
1460
+ }
1461
+ function finishGesture(event) {
1462
+ const gesture = gestureRef.current;
1463
+ if (!gesture || event.pointerId !== gesture.pointerId) return;
1464
+ const finalPreview = previewRef.current;
1465
+ const kind = gesture.kind;
1466
+ const origin = gesture.origin;
1467
+ const originTrackId = gesture.originTrackId;
1468
+ endGestureCleanup();
1469
+ if (!finalPreview || !finalPreview.moved) {
1470
+ props.onSelect(clip.id, event.shiftKey);
1471
+ return;
1472
+ }
1473
+ if (kind === "move") {
1474
+ if (finalPreview.startFrame === origin.startFrame && finalPreview.trackId === originTrackId) return;
1475
+ props.onCommitMove({ clipId: clip.id, startFrame: finalPreview.startFrame, trackId: finalPreview.trackId });
1476
+ return;
1477
+ }
1478
+ if (finalPreview.startFrame === origin.startFrame && finalPreview.durationFrames === origin.durationFrames) return;
1479
+ props.onCommitTrim({
1480
+ clipId: clip.id,
1481
+ startFrame: finalPreview.startFrame,
1482
+ durationFrames: finalPreview.durationFrames,
1483
+ sourceInFrame: finalPreview.sourceInFrame
1484
+ });
1485
+ }
1486
+ useEffect2(() => {
1487
+ if (!preview) return;
1488
+ function onKeyDown(event) {
1489
+ if (event.key !== "Escape") return;
1490
+ event.preventDefault();
1491
+ endGestureCleanup();
1492
+ }
1493
+ window.addEventListener("keydown", onKeyDown);
1494
+ return () => window.removeEventListener("keydown", onKeyDown);
1495
+ }, [preview !== null]);
1496
+ const mediaUrl = clip.media?.url;
1497
+ const mediaKind = clip.media?.kind;
1498
+ const isVisualMedia = mediaKind === "video" || mediaKind === "image";
1499
+ const isAudioMedia = mediaKind === "audio";
1500
+ useEffect2(() => {
1501
+ const canvas = posterRef.current;
1502
+ if (!canvas || !mediaUrl || !isVisualMedia) return;
1503
+ const ctx = canvas.getContext("2d");
1504
+ if (!ctx) return;
1505
+ const dpr = window.devicePixelRatio || 1;
1506
+ const cssWidth = canvas.clientWidth || 40;
1507
+ const cssHeight = canvas.clientHeight || 40;
1508
+ canvas.width = Math.round(cssWidth * dpr);
1509
+ canvas.height = Math.round(cssHeight * dpr);
1510
+ ctx.scale(dpr, dpr);
1511
+ let cancelled = false;
1512
+ frameProvider.drawFrame(mediaUrl, clip.sourceInFrame / fps, ctx, { x: 0, y: 0, width: cssWidth, height: cssHeight }).catch(() => {
1513
+ if (!cancelled) canvas.dataset.posterError = "true";
1514
+ });
1515
+ return () => {
1516
+ cancelled = true;
1517
+ };
1518
+ }, [mediaUrl, isVisualMedia, clip.sourceInFrame, fps, frameProvider]);
1519
+ useEffect2(() => {
1520
+ const canvas = waveformRef.current;
1521
+ if (!canvas || !mediaUrl || !isAudioMedia) return;
1522
+ let pending = waveformCache.get(clip.id);
1523
+ if (!pending) {
1524
+ pending = loadWaveform(mediaUrl, WAVEFORM_BUCKETS);
1525
+ pending.catch(() => waveformCache.delete(clip.id));
1526
+ waveformCache.set(clip.id, pending);
1527
+ }
1528
+ let cancelled = false;
1529
+ pending.then((data) => {
1530
+ if (cancelled) return;
1531
+ const ctx = canvas.getContext("2d");
1532
+ if (!ctx) return;
1533
+ const dpr = window.devicePixelRatio || 1;
1534
+ const cssWidth = canvas.clientWidth || 1;
1535
+ const cssHeight = canvas.clientHeight || 1;
1536
+ canvas.width = Math.round(cssWidth * dpr);
1537
+ canvas.height = Math.round(cssHeight * dpr);
1538
+ ctx.scale(dpr, dpr);
1539
+ const bucketsPerSecond = data.peaks.length / data.durationSeconds;
1540
+ const fromBucket = Math.floor(shown.sourceInFrame / fps * bucketsPerSecond);
1541
+ const toBucket = Math.ceil((shown.sourceInFrame + shown.durationFrames) / fps * bucketsPerSecond);
1542
+ const peaks = data.peaks.subarray(Math.max(0, fromBucket), Math.min(data.peaks.length, Math.max(fromBucket + 1, toBucket)));
1543
+ if (peaks.length === 0) return;
1544
+ drawWaveform(
1545
+ ctx,
1546
+ { peaks, samplesPerBucket: data.samplesPerBucket, durationSeconds: data.durationSeconds },
1547
+ { x: 0, y: 0, width: cssWidth, height: cssHeight },
1548
+ "rgba(52, 211, 153, 0.75)"
1549
+ );
1550
+ }).catch(() => {
1551
+ if (!cancelled) canvas.dataset.waveformError = "true";
1552
+ });
1553
+ return () => {
1554
+ cancelled = true;
1555
+ };
1556
+ }, [mediaUrl, isAudioMedia, clip.id, fps, shown.sourceInFrame, shown.durationFrames, geometry.width]);
1557
+ function commitText() {
1558
+ if (editingText === null) return;
1559
+ const next = editingText.trim();
1560
+ setEditingText(null);
1561
+ if (next.length === 0 || next === clip.text) return;
1562
+ props.onCommitText({ clipId: clip.id, text: next });
1563
+ }
1564
+ const isCaption = track.kind === "caption";
1565
+ const dragging = preview !== null;
1566
+ return /* @__PURE__ */ jsxs3(
1567
+ "div",
1568
+ {
1569
+ ref: rootRef,
1570
+ "data-clip-id": clip.id,
1571
+ role: "button",
1572
+ tabIndex: -1,
1573
+ title: clip.label,
1574
+ className: `group absolute bottom-1 top-1 overflow-hidden rounded border text-left select-none ${KIND_TONES[track.kind]} ${selected ? "ring-2 ring-[var(--brand-primary)]" : "hover:ring-1 hover:ring-[var(--text-muted)]"} ${clip.disabled ? "opacity-40" : ""} ${dragging ? "z-30 shadow-lg shadow-black/30" : ""} ${interactive ? "cursor-grab active:cursor-grabbing" : "cursor-default"}`,
1575
+ style: {
1576
+ left: `${geometry.left}px`,
1577
+ width: `${geometry.width}px`,
1578
+ transform: shown.translateY !== 0 ? `translateY(${shown.translateY}px)` : void 0
1579
+ },
1580
+ onPointerDown: (event) => beginGesture(event, "move"),
1581
+ onPointerMove: updateGesture,
1582
+ onPointerUp: finishGesture,
1583
+ onPointerCancel: endGestureCleanup,
1584
+ onClick: (event) => event.stopPropagation(),
1585
+ onDoubleClick: (event) => {
1586
+ event.stopPropagation();
1587
+ if (isCaption && interactive && typeof clip.text === "string") setEditingText(clip.text);
1588
+ },
1589
+ children: [
1590
+ isAudioMedia ? /* @__PURE__ */ jsx5("canvas", { ref: waveformRef, className: "absolute inset-0 h-full w-full" }) : null,
1591
+ /* @__PURE__ */ jsxs3("div", { className: "relative flex h-full min-w-0 items-stretch gap-1.5 px-1.5 py-1", children: [
1592
+ isVisualMedia ? /* @__PURE__ */ jsx5("canvas", { ref: posterRef, className: "h-full w-10 shrink-0 rounded-sm bg-black/40 object-cover" }) : null,
1593
+ /* @__PURE__ */ jsxs3("div", { className: "min-w-0 flex-1", children: [
1594
+ /* @__PURE__ */ jsx5("div", { className: "truncate text-[11px] font-medium leading-4 text-[var(--text-primary)]", children: isCaption && typeof clip.text === "string" ? clip.text : clip.label }),
1595
+ /* @__PURE__ */ jsx5("span", { className: "mt-0.5 inline-block rounded bg-black/30 px-1 font-mono text-[9px] leading-3 text-[var(--text-secondary)]", children: formatTimecode(shown.durationFrames, fps) })
1596
+ ] })
1597
+ ] }),
1598
+ editingText !== null ? /* @__PURE__ */ jsx5(
1599
+ "input",
1600
+ {
1601
+ autoFocus: true,
1602
+ value: editingText,
1603
+ onChange: (event) => setEditingText(event.target.value),
1604
+ onPointerDown: (event) => event.stopPropagation(),
1605
+ onKeyDown: (event) => {
1606
+ if (event.key === "Enter") commitText();
1607
+ if (event.key === "Escape") setEditingText(null);
1608
+ event.stopPropagation();
1609
+ },
1610
+ onBlur: commitText,
1611
+ className: "absolute inset-0 z-10 w-full bg-black/80 px-1.5 text-[11px] text-[var(--text-primary)] outline-none ring-1 ring-[var(--brand-primary)]",
1612
+ "aria-label": "Caption text"
1613
+ }
1614
+ ) : null,
1615
+ interactive ? /* @__PURE__ */ jsxs3(Fragment, { children: [
1616
+ /* @__PURE__ */ jsx5(
1617
+ "span",
1618
+ {
1619
+ "data-trim-handle": "start",
1620
+ className: "absolute bottom-0 left-0 top-0 z-10 w-1.5 cursor-ew-resize bg-transparent opacity-0 transition group-hover:opacity-100 group-hover:bg-[var(--brand-primary)]/60",
1621
+ onPointerDown: (event) => beginGesture(event, "trim-start"),
1622
+ onPointerMove: updateGesture,
1623
+ onPointerUp: finishGesture,
1624
+ onPointerCancel: endGestureCleanup,
1625
+ "aria-hidden": true
1626
+ }
1627
+ ),
1628
+ /* @__PURE__ */ jsx5(
1629
+ "span",
1630
+ {
1631
+ "data-trim-handle": "end",
1632
+ className: "absolute bottom-0 right-0 top-0 z-10 w-1.5 cursor-ew-resize bg-transparent opacity-0 transition group-hover:opacity-100 group-hover:bg-[var(--brand-primary)]/60",
1633
+ onPointerDown: (event) => beginGesture(event, "trim-end"),
1634
+ onPointerMove: updateGesture,
1635
+ onPointerUp: finishGesture,
1636
+ onPointerCancel: endGestureCleanup,
1637
+ "aria-hidden": true
1638
+ }
1639
+ )
1640
+ ] }) : null
1641
+ ]
1642
+ }
1643
+ );
1644
+ }
1645
+
1646
+ // src/sequences-react/components/glyphs.tsx
1647
+ import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
1648
+ function glyph(paths) {
1649
+ return function Glyph({ className }) {
1650
+ return /* @__PURE__ */ jsx6("svg", { className, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: paths });
1651
+ };
1652
+ }
1653
+ var FilmGlyph = glyph(
1654
+ /* @__PURE__ */ jsxs4(Fragment2, { children: [
1655
+ /* @__PURE__ */ jsx6("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
1656
+ /* @__PURE__ */ jsx6("path", { d: "M7 3v18M17 3v18M3 8h4M3 16h4M17 8h4M17 16h4" })
1657
+ ] })
1658
+ );
1659
+ var AudioGlyph = glyph(
1660
+ /* @__PURE__ */ jsx6("path", { d: "M2 12h2l2-7 3 14 3-9 2 5 2-3h6" })
1661
+ );
1662
+ var CaptionGlyph = glyph(
1663
+ /* @__PURE__ */ jsxs4(Fragment2, { children: [
1664
+ /* @__PURE__ */ jsx6("rect", { x: "2", y: "5", width: "20", height: "14", rx: "2" }),
1665
+ /* @__PURE__ */ jsx6("path", { d: "M6 13h4M6 16h8M14 13h4" })
1666
+ ] })
1667
+ );
1668
+ var ReferenceGlyph = glyph(
1669
+ /* @__PURE__ */ jsxs4(Fragment2, { children: [
1670
+ /* @__PURE__ */ jsx6("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
1671
+ /* @__PURE__ */ jsx6("circle", { cx: "9", cy: "9", r: "2" }),
1672
+ /* @__PURE__ */ jsx6("path", { d: "m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21" })
1673
+ ] })
1674
+ );
1675
+ var AgentGlyph = glyph(
1676
+ /* @__PURE__ */ jsxs4(Fragment2, { children: [
1677
+ /* @__PURE__ */ jsx6("rect", { x: "4", y: "8", width: "16", height: "12", rx: "2" }),
1678
+ /* @__PURE__ */ jsx6("path", { d: "M12 8V4M8 4h8M9 13v2M15 13v2" })
1679
+ ] })
1680
+ );
1681
+ var LockGlyph = glyph(
1682
+ /* @__PURE__ */ jsxs4(Fragment2, { children: [
1683
+ /* @__PURE__ */ jsx6("rect", { x: "5", y: "11", width: "14", height: "10", rx: "2" }),
1684
+ /* @__PURE__ */ jsx6("path", { d: "M8 11V7a4 4 0 0 1 8 0v4" })
1685
+ ] })
1686
+ );
1687
+ var MutedGlyph = glyph(
1688
+ /* @__PURE__ */ jsxs4(Fragment2, { children: [
1689
+ /* @__PURE__ */ jsx6("path", { d: "M11 5 6 9H2v6h4l5 4z" }),
1690
+ /* @__PURE__ */ jsx6("path", { d: "m23 9-6 6M17 9l6 6" })
1691
+ ] })
1692
+ );
1693
+ var PlayGlyph = glyph(
1694
+ /* @__PURE__ */ jsx6("path", { d: "m6 4 14 8-14 8z", fill: "currentColor", stroke: "none" })
1695
+ );
1696
+ var PauseGlyph = glyph(
1697
+ /* @__PURE__ */ jsx6("path", { d: "M7 4h3v16H7zM14 4h3v16h-3z", fill: "currentColor", stroke: "none" })
1698
+ );
1699
+ var UndoGlyph = glyph(
1700
+ /* @__PURE__ */ jsx6("path", { d: "M3 7v6h6M3 13a9 9 0 1 0 3-7.7" })
1701
+ );
1702
+ var RedoGlyph = glyph(
1703
+ /* @__PURE__ */ jsx6("path", { d: "M21 7v6h-6M21 13a9 9 0 1 1-3-7.7" })
1704
+ );
1705
+ var MagnetGlyph = glyph(
1706
+ /* @__PURE__ */ jsxs4(Fragment2, { children: [
1707
+ /* @__PURE__ */ jsx6("path", { d: "m6 15-4-4 6.75-6.77a7.79 7.79 0 0 1 11 11L13 22l-4-4 6.39-6.36a2.14 2.14 0 0 0-3-3z" }),
1708
+ /* @__PURE__ */ jsx6("path", { d: "m5 8 4 4M12 15l4 4" })
1709
+ ] })
1710
+ );
1711
+ var ScissorsGlyph = glyph(
1712
+ /* @__PURE__ */ jsxs4(Fragment2, { children: [
1713
+ /* @__PURE__ */ jsx6("circle", { cx: "6", cy: "6", r: "3" }),
1714
+ /* @__PURE__ */ jsx6("circle", { cx: "6", cy: "18", r: "3" }),
1715
+ /* @__PURE__ */ jsx6("path", { d: "M20 4 8.12 15.88M14.47 14.48 20 20M8.12 8.12 12 12" })
1716
+ ] })
1717
+ );
1718
+ var CaptionPlusGlyph = glyph(
1719
+ /* @__PURE__ */ jsxs4(Fragment2, { children: [
1720
+ /* @__PURE__ */ jsx6("rect", { x: "2", y: "5", width: "20", height: "14", rx: "2" }),
1721
+ /* @__PURE__ */ jsx6("path", { d: "M12 9v6M9 12h6" })
1722
+ ] })
1723
+ );
1724
+
1725
+ // src/sequences-react/components/TimelineTrackRow.tsx
1726
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
1727
+ var LANE_HEIGHTS = {
1728
+ video: "h-16",
1729
+ reference: "h-16",
1730
+ audio: "h-14",
1731
+ caption: "h-9",
1732
+ agent: "h-9"
1733
+ };
1734
+ var KIND_GLYPHS = {
1735
+ video: FilmGlyph,
1736
+ audio: AudioGlyph,
1737
+ caption: CaptionGlyph,
1738
+ reference: ReferenceGlyph,
1739
+ agent: AgentGlyph
1740
+ };
1741
+ function TimelineTrackRow(props) {
1742
+ const { track, clips, fps, zoom, sequenceDurationFrames } = props;
1743
+ const Glyph = KIND_GLYPHS[track.kind];
1744
+ const laneHeight = LANE_HEIGHTS[track.kind];
1745
+ function handleLanePointerDown(event) {
1746
+ if (event.button !== 0) return;
1747
+ const rect = event.currentTarget.getBoundingClientRect();
1748
+ const frame = Math.max(0, Math.min(sequenceDurationFrames, Math.round((event.clientX - rect.left) / zoom)));
1749
+ props.onLaneSeek(frame);
1750
+ }
1751
+ return /* @__PURE__ */ jsxs5("div", { className: "flex border-b border-[var(--border-default)] last:border-b-0", children: [
1752
+ /* @__PURE__ */ jsxs5("div", { className: `sticky left-0 z-10 flex w-36 shrink-0 items-center gap-2 border-r border-[var(--border-default)] bg-[var(--bg-input)] px-2.5 ${laneHeight}`, children: [
1753
+ /* @__PURE__ */ jsx7(Glyph, { className: "h-3.5 w-3.5 shrink-0 text-[var(--text-muted)]" }),
1754
+ /* @__PURE__ */ jsx7("span", { className: "min-w-0 flex-1 truncate text-xs font-medium text-[var(--text-secondary)]", children: track.name }),
1755
+ track.locked ? /* @__PURE__ */ jsx7(LockGlyph, { className: "h-3 w-3 shrink-0 text-amber-400" }) : null,
1756
+ track.muted ? /* @__PURE__ */ jsx7(MutedGlyph, { className: "h-3 w-3 shrink-0 text-[var(--text-muted)]" }) : null
1757
+ ] }),
1758
+ /* @__PURE__ */ jsx7(
1759
+ "div",
1760
+ {
1761
+ "data-lane-track": track.id,
1762
+ "data-lane-kind": track.kind,
1763
+ "data-lane-locked": track.locked ? "true" : "false",
1764
+ className: `relative ${laneHeight} ${track.muted ? "opacity-60" : ""}`,
1765
+ style: { width: `${sequenceDurationFrames * zoom}px` },
1766
+ onPointerDown: handleLanePointerDown,
1767
+ children: clips.map((clip) => /* @__PURE__ */ jsx7(
1768
+ TimelineClipChip,
1769
+ {
1770
+ clip,
1771
+ track,
1772
+ fps,
1773
+ zoom,
1774
+ sequenceDurationFrames,
1775
+ selected: props.selectedClipIds.has(clip.id),
1776
+ canWrite: props.canWrite,
1777
+ frameProvider: props.frameProvider,
1778
+ snapMove: props.snapMove,
1779
+ snapEdge: props.snapEdge,
1780
+ onSnapPointChange: props.onSnapPointChange,
1781
+ onSelect: props.onSelectClip,
1782
+ onCommitMove: props.onCommitMove,
1783
+ onCommitTrim: props.onCommitTrim,
1784
+ onCommitText: props.onCommitText
1785
+ },
1786
+ clip.id
1787
+ ))
1788
+ }
1789
+ )
1790
+ ] });
1791
+ }
1792
+
1793
+ // src/sequences-react/components/ZoomControl.tsx
1794
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
1795
+ function ZoomControl({ zoomMath, zoom, onZoomChange }) {
1796
+ const sliderMin = zoomMath.zoomToSlider(zoomMath.minZoom);
1797
+ const sliderMax = zoomMath.zoomToSlider(zoomMath.maxZoom);
1798
+ const sliderStep = (sliderMax - sliderMin) / 100;
1799
+ const slider = zoomMath.zoomToSlider(zoom);
1800
+ function setSlider(next) {
1801
+ const clamped = Math.max(sliderMin, Math.min(sliderMax, next));
1802
+ onZoomChange(zoomMath.sliderToZoom(clamped));
1803
+ }
1804
+ return /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-1.5", children: [
1805
+ /* @__PURE__ */ jsx8(
1806
+ "button",
1807
+ {
1808
+ type: "button",
1809
+ "aria-label": "Zoom out",
1810
+ onClick: () => setSlider(slider - sliderStep * 10),
1811
+ className: "flex h-6 w-6 items-center justify-center rounded border border-[var(--border-default)] text-sm leading-none text-[var(--text-secondary)] hover:text-[var(--text-primary)]",
1812
+ children: "\u2212"
1813
+ }
1814
+ ),
1815
+ /* @__PURE__ */ jsx8(
1816
+ "input",
1817
+ {
1818
+ type: "range",
1819
+ "aria-label": "Timeline zoom",
1820
+ min: sliderMin,
1821
+ max: sliderMax,
1822
+ step: sliderStep,
1823
+ value: slider,
1824
+ onChange: (event) => setSlider(Number(event.target.value)),
1825
+ className: "h-1 w-24 cursor-pointer accent-[var(--brand-primary)]"
1826
+ }
1827
+ ),
1828
+ /* @__PURE__ */ jsx8(
1829
+ "button",
1830
+ {
1831
+ type: "button",
1832
+ "aria-label": "Zoom in",
1833
+ onClick: () => setSlider(slider + sliderStep * 10),
1834
+ className: "flex h-6 w-6 items-center justify-center rounded border border-[var(--border-default)] text-sm leading-none text-[var(--text-secondary)] hover:text-[var(--text-primary)]",
1835
+ children: "+"
1836
+ }
1837
+ )
1838
+ ] });
1839
+ }
1840
+
1841
+ // src/sequences-react/components/TimelineEditor.tsx
1842
+ import { Fragment as Fragment3, jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
1843
+ var SEQUENCE_MEDIA_DRAG_TYPE = "application/x-sequence-media";
1844
+ var HISTORY_MIRROR_LIMIT = 200;
1845
+ var MIN_ZOOM = 5e-3;
1846
+ var MAX_ZOOM = 24;
1847
+ var TRACK_HEADER_PX = 144;
1848
+ var TRANSPORT_BUTTON = "flex h-7 w-7 items-center justify-center rounded border border-[var(--border-default)] text-[var(--text-secondary)] transition hover:text-[var(--text-primary)] disabled:cursor-default disabled:opacity-40 disabled:hover:text-[var(--text-secondary)]";
1849
+ function mintClipId() {
1850
+ const uuid = globalThis.crypto && "randomUUID" in globalThis.crypto ? globalThis.crypto.randomUUID() : null;
1851
+ return `local-${uuid ?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`}`;
1852
+ }
1853
+ function isTypingTarget(target) {
1854
+ return target instanceof Element && target.closest('input, textarea, select, button, [contenteditable="true"]') !== null;
1855
+ }
1856
+ function parseMediaDragPayload(raw) {
1857
+ const parsed = JSON.parse(raw);
1858
+ if (typeof parsed.url !== "string" || parsed.url.length === 0) {
1859
+ throw new Error(`${SEQUENCE_MEDIA_DRAG_TYPE} payload requires a non-empty url`);
1860
+ }
1861
+ if (parsed.kind !== "video" && parsed.kind !== "image" && parsed.kind !== "audio") {
1862
+ throw new Error(`${SEQUENCE_MEDIA_DRAG_TYPE} payload kind must be video | image | audio, got ${String(parsed.kind)}`);
1863
+ }
1864
+ return parsed;
1865
+ }
1866
+ function TimelineEditor(props) {
1867
+ const { canWrite, onApplyOperations } = props;
1868
+ const fps = props.timeline.sequence.fps;
1869
+ const durationFrames = props.timeline.sequence.durationFrames;
1870
+ const stack = useMemo3(() => createCommandStack(props.timeline), []);
1871
+ const editorState = useSyncExternalStore(stack.subscribe, stack.getState, stack.getState);
1872
+ const timeline = editorState.timeline;
1873
+ const appliedTimelineRef = useRef3(props.timeline);
1874
+ useEffect3(() => {
1875
+ if (appliedTimelineRef.current === props.timeline) return;
1876
+ appliedTimelineRef.current = props.timeline;
1877
+ stack.reset(props.timeline);
1878
+ }, [props.timeline, stack]);
1879
+ const clock = useMemo3(
1880
+ () => createPlaybackClock({ fps, durationFrames: timeline.sequence.durationFrames }),
1881
+ [fps, timeline.sequence.durationFrames]
1882
+ );
1883
+ useEffect3(() => () => clock.dispose(), [clock]);
1884
+ const [playheadFrame, setPlayheadFrame] = useState3(0);
1885
+ const [isPlaying, setIsPlaying] = useState3(false);
1886
+ const onPlayheadChangeRef = useRef3(props.onPlayheadChange);
1887
+ onPlayheadChangeRef.current = props.onPlayheadChange;
1888
+ useEffect3(() => {
1889
+ clock.seek(Math.min(playheadFrame, timeline.sequence.durationFrames - 1));
1890
+ return clock.subscribe((frame) => {
1891
+ setPlayheadFrame(frame);
1892
+ setIsPlaying(clock.isPlaying());
1893
+ onPlayheadChangeRef.current?.(frame);
1894
+ });
1895
+ }, [clock]);
1896
+ const ownedProviderRef = useRef3(null);
1897
+ const frameProvider = useMemo3(() => {
1898
+ if (props.frameProvider) return props.frameProvider;
1899
+ if (!ownedProviderRef.current) ownedProviderRef.current = createVideoElementFrameProvider();
1900
+ return ownedProviderRef.current;
1901
+ }, [props.frameProvider]);
1902
+ useEffect3(
1903
+ () => () => {
1904
+ ownedProviderRef.current?.dispose();
1905
+ ownedProviderRef.current = null;
1906
+ },
1907
+ []
1908
+ );
1909
+ const zoomMath = useMemo3(() => createZoomMath({ minZoom: MIN_ZOOM, maxZoom: MAX_ZOOM }), []);
1910
+ const [zoom, setZoom] = useState3(1);
1911
+ const trackViewportRef = useRef3(null);
1912
+ useEffect3(() => {
1913
+ const viewport = trackViewportRef.current;
1914
+ if (!viewport) return;
1915
+ const laneWidth = viewport.clientWidth - TRACK_HEADER_PX;
1916
+ if (laneWidth <= 0) return;
1917
+ setZoom(Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, laneWidth / durationFrames)));
1918
+ }, []);
1919
+ const [snapEnabled, setSnapEnabled] = useState3(true);
1920
+ const [activeSnapPoint, setActiveSnapPoint] = useState3(null);
1921
+ const [selectedClipIds, setSelectedClipIds] = useState3([]);
1922
+ const [commitError, setCommitError] = useState3(null);
1923
+ const selectedClips = useMemo3(
1924
+ () => timeline.clips.filter((clip) => selectedClipIds.includes(clip.id)),
1925
+ [timeline, selectedClipIds]
1926
+ );
1927
+ const onSelectionChangeRef = useRef3(props.onSelectionChange);
1928
+ onSelectionChangeRef.current = props.onSelectionChange;
1929
+ useEffect3(() => {
1930
+ onSelectionChangeRef.current?.(selectedClips);
1931
+ }, [selectedClips]);
1932
+ const sortedTracks = useMemo3(() => [...timeline.tracks].sort((a, b) => a.sortOrder - b.sortOrder), [timeline.tracks]);
1933
+ const clipsByTrack = useMemo3(() => {
1934
+ const byTrack = /* @__PURE__ */ new Map();
1935
+ for (const clip of timeline.clips) {
1936
+ const bucket = byTrack.get(clip.trackId);
1937
+ if (bucket) bucket.push(clip);
1938
+ else byTrack.set(clip.trackId, [clip]);
1939
+ }
1940
+ return byTrack;
1941
+ }, [timeline.clips]);
1942
+ const historyRef = useRef3({ done: [], undone: [] });
1943
+ const clipIdAliasesRef = useRef3(/* @__PURE__ */ new Map());
1944
+ function resolveClipId(clipId) {
1945
+ return clipIdAliasesRef.current.get(clipId) ?? clipId;
1946
+ }
1947
+ function reconcileCreatedClipIds(operations, createdLocalIds, results) {
1948
+ if (createdLocalIds.length === 0 || !Array.isArray(results)) return;
1949
+ if (results.length !== operations.length) {
1950
+ setCommitError(
1951
+ `onApplyOperations returned ${results.length} results for ${operations.length} operations \u2014 clip-id reconciliation skipped`
1952
+ );
1953
+ return;
1954
+ }
1955
+ let cursor = 0;
1956
+ operations.forEach((operation, index) => {
1957
+ if (operation.type !== "place_clip" && operation.type !== "add_caption" && operation.type !== "split_clip") return;
1958
+ const localId = createdLocalIds[cursor];
1959
+ cursor += 1;
1960
+ const result = results[index];
1961
+ if (localId !== void 0 && result !== void 0 && result.kind === "clip") {
1962
+ clipIdAliasesRef.current.set(localId, result.clip.id);
1963
+ }
1964
+ });
1965
+ }
1966
+ function commitCommand(command, createdLocalIds = []) {
1967
+ if (!canWrite) return;
1968
+ try {
1969
+ stack.execute(command);
1970
+ } catch (error) {
1971
+ setCommitError(error instanceof Error ? error.message : String(error));
1972
+ return;
1973
+ }
1974
+ const history = historyRef.current;
1975
+ const entry = { command, createdLocalIds };
1976
+ history.done.push(entry);
1977
+ if (history.done.length > HISTORY_MIRROR_LIMIT) history.done.splice(0, history.done.length - HISTORY_MIRROR_LIMIT);
1978
+ history.undone = [];
1979
+ setCommitError(null);
1980
+ const operations = command.operations();
1981
+ void onApplyOperations(operations).then((results) => reconcileCreatedClipIds(operations, createdLocalIds, results)).catch((error) => {
1982
+ const mirror = historyRef.current;
1983
+ if (mirror.done[mirror.done.length - 1] === entry && stack.canUndo()) {
1984
+ stack.undo();
1985
+ mirror.done.pop();
1986
+ }
1987
+ setCommitError(error instanceof Error ? error.message : String(error));
1988
+ });
1989
+ }
1990
+ function undoLast() {
1991
+ const history = historyRef.current;
1992
+ const entry = history.done[history.done.length - 1];
1993
+ if (!entry || !stack.canUndo()) return;
1994
+ try {
1995
+ stack.undo();
1996
+ } catch (error) {
1997
+ setCommitError(`Undo failed: ${error instanceof Error ? error.message : String(error)}`);
1998
+ return;
1999
+ }
2000
+ history.done.pop();
2001
+ history.undone.push(entry);
2002
+ void onApplyOperations(entry.command.inverseOperations()).catch((error) => {
2003
+ setCommitError(error instanceof Error ? error.message : String(error));
2004
+ });
2005
+ }
2006
+ function redoLast() {
2007
+ const history = historyRef.current;
2008
+ const entry = history.undone[history.undone.length - 1];
2009
+ if (!entry || !stack.canRedo()) return;
2010
+ try {
2011
+ stack.redo();
2012
+ } catch (error) {
2013
+ setCommitError(`Redo failed: ${error instanceof Error ? error.message : String(error)}`);
2014
+ return;
2015
+ }
2016
+ history.undone.pop();
2017
+ history.done.push(entry);
2018
+ const operations = entry.command.operations();
2019
+ void onApplyOperations(operations).then((results) => reconcileCreatedClipIds(operations, entry.createdLocalIds, results)).catch((error) => {
2020
+ setCommitError(error instanceof Error ? error.message : String(error));
2021
+ });
2022
+ }
2023
+ function handleCommitMove(input) {
2024
+ const current = stack.getState().timeline;
2025
+ const clip = current.clips.find((candidate) => candidate.id === input.clipId);
2026
+ if (!clip) return;
2027
+ commitCommand(
2028
+ moveClipCommand({
2029
+ timeline: current,
2030
+ clipId: input.clipId,
2031
+ startFrame: input.startFrame,
2032
+ ...input.trackId !== clip.trackId ? { trackId: input.trackId } : {},
2033
+ resolveClipId
2034
+ })
2035
+ );
2036
+ }
2037
+ function handleCommitTrim(input) {
2038
+ commitCommand(
2039
+ trimClipCommand({
2040
+ timeline: stack.getState().timeline,
2041
+ clipId: input.clipId,
2042
+ startFrame: input.startFrame,
2043
+ durationFrames: input.durationFrames,
2044
+ sourceInFrame: input.sourceInFrame,
2045
+ resolveClipId
2046
+ })
2047
+ );
2048
+ }
2049
+ function handleCommitText(input) {
2050
+ commitCommand(
2051
+ setClipTextCommand({ timeline: stack.getState().timeline, clipId: input.clipId, text: input.text, resolveClipId })
2052
+ );
2053
+ }
2054
+ function deleteSelection() {
2055
+ const current = stack.getState().timeline;
2056
+ const lockedTrackIds = new Set(current.tracks.filter((track) => track.locked).map((track) => track.id));
2057
+ const targets = selectedClips.filter((clip) => !lockedTrackIds.has(clip.trackId));
2058
+ if (targets.length === 0) return;
2059
+ const commands = targets.map((clip) => deleteClipCommand({ timeline: current, clipId: clip.id, resolveClipId }));
2060
+ commitCommand(commands.length === 1 ? commands[0] : compositeCommand(`Delete ${commands.length} clips`, commands));
2061
+ setSelectedClipIds([]);
2062
+ }
2063
+ const splittableClip = selectedClips.length === 1 && selectedClips[0] && playheadFrame > selectedClips[0].startFrame && playheadFrame < selectedClips[0].startFrame + selectedClips[0].durationFrames ? selectedClips[0] : null;
2064
+ function splitAtPlayhead() {
2065
+ if (!splittableClip) return;
2066
+ const newClipId = mintClipId();
2067
+ commitCommand(
2068
+ splitClipCommand({
2069
+ timeline: stack.getState().timeline,
2070
+ clipId: splittableClip.id,
2071
+ atFrame: playheadFrame,
2072
+ newClipId,
2073
+ resolveClipId
2074
+ }),
2075
+ [newClipId]
2076
+ );
2077
+ }
2078
+ function addCaptionAtPlayhead() {
2079
+ const current = stack.getState().timeline;
2080
+ const captionTrack = [...current.tracks].sort((a, b) => a.sortOrder - b.sortOrder).find((track) => track.kind === "caption" && !track.locked);
2081
+ if (!captionTrack) {
2082
+ setCommitError("No unlocked caption track in this sequence \u2014 add one before inserting captions.");
2083
+ return;
2084
+ }
2085
+ let placement;
2086
+ try {
2087
+ placement = chooseCaptionPlacement({
2088
+ playheadFrame,
2089
+ fps,
2090
+ sequenceDurationFrames: current.sequence.durationFrames,
2091
+ occupiedIntervals: trackIntervals(current, captionTrack.id)
2092
+ });
2093
+ } catch (error) {
2094
+ setCommitError(error instanceof Error ? error.message : String(error));
2095
+ return;
2096
+ }
2097
+ const clipId = mintClipId();
2098
+ commitCommand(
2099
+ addCaptionCommand({
2100
+ timeline: current,
2101
+ clipId,
2102
+ trackId: captionTrack.id,
2103
+ text: "New caption",
2104
+ startFrame: placement.startFrame,
2105
+ durationFrames: placement.durationFrames,
2106
+ resolveClipId
2107
+ }),
2108
+ [clipId]
2109
+ );
2110
+ }
2111
+ function selectClip(clipId, additive) {
2112
+ setSelectedClipIds((current) => {
2113
+ if (!additive) return [clipId];
2114
+ return current.includes(clipId) ? current.filter((id) => id !== clipId) : [...current, clipId];
2115
+ });
2116
+ }
2117
+ function snapMove(candidate) {
2118
+ if (!snapEnabled) return { startFrame: candidate.startFrame, point: null };
2119
+ const points = collectSnapPoints(timeline, playheadFrame);
2120
+ const exclude = (point) => point.clipId === candidate.clipId;
2121
+ return chooseMoveSnap({
2122
+ candidateStartFrame: candidate.startFrame,
2123
+ durationFrames: candidate.durationFrames,
2124
+ startSnap: applySnap(candidate.startFrame, points, { zoom, exclude }),
2125
+ endSnap: applySnap(candidate.startFrame + candidate.durationFrames, points, { zoom, exclude })
2126
+ });
2127
+ }
2128
+ function snapEdge(candidate) {
2129
+ if (!snapEnabled) return { frame: candidate.frame, point: null };
2130
+ const points = collectSnapPoints(timeline, playheadFrame);
2131
+ const result = applySnap(candidate.frame, points, {
2132
+ zoom,
2133
+ exclude: (point) => point.clipId === candidate.clipId
2134
+ });
2135
+ return { frame: result.frame, point: result.point };
2136
+ }
2137
+ function togglePlayback() {
2138
+ if (clock.isPlaying()) clock.pause();
2139
+ else clock.play();
2140
+ setIsPlaying(clock.isPlaying());
2141
+ }
2142
+ useEffect3(() => {
2143
+ function onKeyDown(event) {
2144
+ if (event.code === "Space" && !isTypingTarget(event.target)) {
2145
+ event.preventDefault();
2146
+ togglePlayback();
2147
+ return;
2148
+ }
2149
+ if ((event.key === "Delete" || event.key === "Backspace") && !isTypingTarget(event.target)) {
2150
+ if (!canWrite) return;
2151
+ event.preventDefault();
2152
+ deleteSelection();
2153
+ return;
2154
+ }
2155
+ const mod = event.metaKey || event.ctrlKey;
2156
+ if (!mod || isTypingTarget(event.target)) return;
2157
+ if (event.key.toLowerCase() === "z") {
2158
+ event.preventDefault();
2159
+ if (event.shiftKey) redoLast();
2160
+ else undoLast();
2161
+ } else if (event.key.toLowerCase() === "y") {
2162
+ event.preventDefault();
2163
+ redoLast();
2164
+ }
2165
+ }
2166
+ window.addEventListener("keydown", onKeyDown);
2167
+ return () => window.removeEventListener("keydown", onKeyDown);
2168
+ });
2169
+ function handleTrackAreaDragOver(event) {
2170
+ if (!canWrite || !event.dataTransfer.types.includes(SEQUENCE_MEDIA_DRAG_TYPE)) return;
2171
+ event.preventDefault();
2172
+ event.dataTransfer.dropEffect = "copy";
2173
+ }
2174
+ function handleTrackAreaDrop(event) {
2175
+ if (!canWrite || !event.dataTransfer.types.includes(SEQUENCE_MEDIA_DRAG_TYPE)) return;
2176
+ event.preventDefault();
2177
+ const lane = event.target instanceof Element ? event.target.closest("[data-lane-track]") : null;
2178
+ if (!lane || !lane.dataset.laneTrack) {
2179
+ setCommitError("Drop media on a track lane to place it.");
2180
+ return;
2181
+ }
2182
+ let payload;
2183
+ try {
2184
+ payload = parseMediaDragPayload(event.dataTransfer.getData(SEQUENCE_MEDIA_DRAG_TYPE));
2185
+ } catch (error) {
2186
+ setCommitError(error instanceof Error ? error.message : String(error));
2187
+ return;
2188
+ }
2189
+ const laneKind = lane.dataset.laneKind;
2190
+ const accepts = laneKind === "video" ? payload.kind === "video" || payload.kind === "image" : laneKind === "audio" ? payload.kind === "audio" : false;
2191
+ if (!accepts || lane.dataset.laneLocked === "true") {
2192
+ setCommitError(`A ${laneKind ?? "unknown"} track cannot take ${payload.kind} media${lane.dataset.laneLocked === "true" ? " (track is locked)" : ""}.`);
2193
+ return;
2194
+ }
2195
+ const current = stack.getState().timeline;
2196
+ const rect = lane.getBoundingClientRect();
2197
+ const dropFrame = Math.max(0, Math.round((event.clientX - rect.left) / zoom));
2198
+ const naturalDuration = payload.durationSeconds !== void 0 ? secondsToFrames(payload.durationSeconds, fps) : payload.kind === "image" ? fps * 3 : fps * 5;
2199
+ const startFrame = Math.min(dropFrame, current.sequence.durationFrames - 1);
2200
+ const placedDuration = Math.max(1, Math.min(naturalDuration, current.sequence.durationFrames - startFrame));
2201
+ const clipId = mintClipId();
2202
+ commitCommand(
2203
+ placeClipCommand({
2204
+ timeline: current,
2205
+ clipId,
2206
+ trackId: lane.dataset.laneTrack,
2207
+ label: payload.label ?? payload.url.split("/").pop() ?? payload.url,
2208
+ startFrame,
2209
+ durationFrames: placedDuration,
2210
+ media: { url: payload.url, kind: payload.kind },
2211
+ ...payload.generationId !== void 0 ? { generationId: payload.generationId } : {},
2212
+ ...payload.assetId !== void 0 ? { assetId: payload.assetId } : {},
2213
+ resolveClipId
2214
+ }),
2215
+ [clipId]
2216
+ );
2217
+ }
2218
+ const timelineWidth = timeline.sequence.durationFrames * zoom;
2219
+ return /* @__PURE__ */ jsx9("div", { className: `flex h-full min-h-0 flex-col bg-[var(--bg-input)] text-[var(--text-primary)] ${props.className ?? ""}`, children: /* @__PURE__ */ jsxs7("div", { className: "flex min-h-0 flex-1", children: [
2220
+ /* @__PURE__ */ jsxs7("div", { className: "flex min-w-0 flex-1 flex-col", children: [
2221
+ /* @__PURE__ */ jsx9(PreviewCanvas, { timeline, clock, frameProvider }),
2222
+ /* @__PURE__ */ jsxs7("div", { className: "flex h-10 shrink-0 items-center gap-2 border-y border-[var(--border-default)] px-2", children: [
2223
+ /* @__PURE__ */ jsx9(
2224
+ "button",
2225
+ {
2226
+ type: "button",
2227
+ "aria-label": isPlaying ? "Pause" : "Play",
2228
+ onClick: togglePlayback,
2229
+ className: TRANSPORT_BUTTON,
2230
+ children: isPlaying ? /* @__PURE__ */ jsx9(PauseGlyph, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx9(PlayGlyph, { className: "h-3.5 w-3.5" })
2231
+ }
2232
+ ),
2233
+ /* @__PURE__ */ jsxs7("span", { className: "font-mono text-xs tabular-nums text-[var(--text-secondary)]", children: [
2234
+ formatTimecode(playheadFrame, fps),
2235
+ /* @__PURE__ */ jsxs7("span", { className: "text-[var(--text-muted)]", children: [
2236
+ " / ",
2237
+ formatTimecode(timeline.sequence.durationFrames, fps)
2238
+ ] })
2239
+ ] }),
2240
+ /* @__PURE__ */ jsx9("div", { className: "mx-1 h-4 w-px bg-[var(--border-default)]" }),
2241
+ /* @__PURE__ */ jsx9("button", { type: "button", "aria-label": "Undo", disabled: !stack.canUndo() || !canWrite, onClick: undoLast, className: TRANSPORT_BUTTON, children: /* @__PURE__ */ jsx9(UndoGlyph, { className: "h-3.5 w-3.5" }) }),
2242
+ /* @__PURE__ */ jsx9("button", { type: "button", "aria-label": "Redo", disabled: !stack.canRedo() || !canWrite, onClick: redoLast, className: TRANSPORT_BUTTON, children: /* @__PURE__ */ jsx9(RedoGlyph, { className: "h-3.5 w-3.5" }) }),
2243
+ /* @__PURE__ */ jsx9(
2244
+ "button",
2245
+ {
2246
+ type: "button",
2247
+ "aria-label": "Toggle snapping",
2248
+ "aria-pressed": snapEnabled,
2249
+ onClick: () => setSnapEnabled((current) => !current),
2250
+ className: `${TRANSPORT_BUTTON} ${snapEnabled ? "border-[var(--brand-primary)] text-[var(--brand-primary)] hover:text-[var(--brand-primary)]" : ""}`,
2251
+ children: /* @__PURE__ */ jsx9(MagnetGlyph, { className: "h-3.5 w-3.5" })
2252
+ }
2253
+ ),
2254
+ canWrite ? /* @__PURE__ */ jsxs7(Fragment3, { children: [
2255
+ /* @__PURE__ */ jsx9("button", { type: "button", "aria-label": "Split clip at playhead", disabled: !splittableClip, onClick: splitAtPlayhead, className: TRANSPORT_BUTTON, children: /* @__PURE__ */ jsx9(ScissorsGlyph, { className: "h-3.5 w-3.5" }) }),
2256
+ /* @__PURE__ */ jsx9("button", { type: "button", "aria-label": "Add caption at playhead", onClick: addCaptionAtPlayhead, className: TRANSPORT_BUTTON, children: /* @__PURE__ */ jsx9(CaptionPlusGlyph, { className: "h-3.5 w-3.5" }) })
2257
+ ] }) : null,
2258
+ /* @__PURE__ */ jsx9("div", { className: "flex-1" }),
2259
+ /* @__PURE__ */ jsx9(ZoomControl, { zoomMath, zoom, onZoomChange: setZoom })
2260
+ ] }),
2261
+ commitError ? /* @__PURE__ */ jsxs7("div", { className: "flex shrink-0 items-center justify-between gap-3 border-b border-rose-500/30 bg-rose-500/10 px-3 py-1.5 text-xs text-rose-300", role: "alert", children: [
2262
+ /* @__PURE__ */ jsx9("span", { className: "min-w-0 truncate", children: commitError }),
2263
+ /* @__PURE__ */ jsx9("button", { type: "button", onClick: () => setCommitError(null), className: "shrink-0 underline-offset-2 hover:underline", children: "Dismiss" })
2264
+ ] }) : null,
2265
+ props.renderAssetShelf ? /* @__PURE__ */ jsx9("div", { className: "shrink-0 border-b border-[var(--border-default)]", children: props.renderAssetShelf() }) : null,
2266
+ /* @__PURE__ */ jsx9(
2267
+ "div",
2268
+ {
2269
+ ref: trackViewportRef,
2270
+ "data-timeline-tracks": true,
2271
+ className: "relative max-h-60 min-h-[6rem] shrink-0 overflow-auto overscroll-x-contain",
2272
+ onDragOver: handleTrackAreaDragOver,
2273
+ onDrop: handleTrackAreaDrop,
2274
+ children: /* @__PURE__ */ jsxs7("div", { className: "relative", style: { width: `${TRACK_HEADER_PX + timelineWidth}px`, minWidth: "100%" }, children: [
2275
+ /* @__PURE__ */ jsxs7("div", { className: "sticky top-0 z-20 flex", children: [
2276
+ /* @__PURE__ */ jsx9("div", { className: "sticky left-0 z-30 w-36 shrink-0 border-b border-r border-[var(--border-default)] bg-[var(--bg-input)]" }),
2277
+ /* @__PURE__ */ jsx9(TimelineRuler, { fps, durationFrames: timeline.sequence.durationFrames, zoom, onScrub: (frame) => clock.seek(frame) })
2278
+ ] }),
2279
+ /* @__PURE__ */ jsxs7("div", { className: "relative", children: [
2280
+ sortedTracks.map((track) => /* @__PURE__ */ jsx9(
2281
+ TimelineTrackRow,
2282
+ {
2283
+ track,
2284
+ clips: clipsByTrack.get(track.id) ?? [],
2285
+ fps,
2286
+ zoom,
2287
+ sequenceDurationFrames: timeline.sequence.durationFrames,
2288
+ selectedClipIds: new Set(selectedClipIds),
2289
+ canWrite,
2290
+ frameProvider,
2291
+ snapMove,
2292
+ snapEdge,
2293
+ onSnapPointChange: setActiveSnapPoint,
2294
+ onSelectClip: selectClip,
2295
+ onCommitMove: handleCommitMove,
2296
+ onCommitTrim: handleCommitTrim,
2297
+ onCommitText: handleCommitText,
2298
+ onLaneSeek: (frame) => clock.seek(frame)
2299
+ },
2300
+ track.id
2301
+ )),
2302
+ /* @__PURE__ */ jsxs7("div", { className: "pointer-events-none absolute inset-y-0", style: { left: `${TRACK_HEADER_PX}px`, width: `${timelineWidth}px` }, children: [
2303
+ /* @__PURE__ */ jsx9(TimelinePlayhead, { frame: playheadFrame, zoom }),
2304
+ /* @__PURE__ */ jsx9(SnapIndicatorLine, { point: activeSnapPoint, zoom })
2305
+ ] })
2306
+ ] })
2307
+ ] })
2308
+ }
2309
+ )
2310
+ ] }),
2311
+ props.renderSidePanel ? /* @__PURE__ */ jsx9("aside", { className: "flex w-80 shrink-0 flex-col overflow-hidden border-l border-[var(--border-default)]", children: props.renderSidePanel({ selectedClips, playheadFrame }) }) : null
2312
+ ] }) });
2313
+ }
2314
+ var TimelineEditor_default = TimelineEditor;
2315
+
2316
+ export {
2317
+ COMMAND_HISTORY_LIMIT,
2318
+ createCommandStack,
2319
+ moveClipCommand,
2320
+ trimClipCommand,
2321
+ placeClipCommand,
2322
+ deleteClipCommand,
2323
+ splitClipCommand,
2324
+ addCaptionCommand,
2325
+ setClipTextCommand,
2326
+ toggleClipDisabledCommand,
2327
+ createZoomMath,
2328
+ frameToPixel,
2329
+ pixelToFrame,
2330
+ snapPixel,
2331
+ collectSnapPoints,
2332
+ applySnap,
2333
+ createPlaybackClock,
2334
+ DEFAULT_MAX_MEDIA_ELEMENTS,
2335
+ SEEK_TOLERANCE_SECONDS,
2336
+ SEEK_TIMEOUT_MS,
2337
+ containFitRect,
2338
+ needsSeek,
2339
+ createMediaElementPool,
2340
+ classifyMediaUrl,
2341
+ createImageFrameProvider,
2342
+ createVideoElementFrameProvider,
2343
+ computeWaveform,
2344
+ loadWaveform,
2345
+ drawWaveform,
2346
+ compositeCommand,
2347
+ framesFromPixelDelta,
2348
+ moveDragStartFrame,
2349
+ trimStartDrag,
2350
+ trimEndDrag,
2351
+ selectTickStepSeconds,
2352
+ letterboxRect,
2353
+ captionFontPx,
2354
+ clipChipGeometry,
2355
+ chooseMoveSnap,
2356
+ PreviewCanvas,
2357
+ SnapIndicatorLine,
2358
+ TimelinePlayhead,
2359
+ TimelineRuler,
2360
+ TimelineClipChip,
2361
+ TimelineTrackRow,
2362
+ ZoomControl,
2363
+ SEQUENCE_MEDIA_DRAG_TYPE,
2364
+ TimelineEditor,
2365
+ TimelineEditor_default
2366
+ };
2367
+ //# sourceMappingURL=chunk-IHR6K3GF.js.map