@webpacked-timeline/core 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +21 -0
  3. package/README.md +162 -0
  4. package/dist/chunk-27XCNVPR.js +5969 -0
  5. package/dist/chunk-6PDBJDHM.js +2263 -0
  6. package/dist/chunk-BWPS6NQT.js +7465 -0
  7. package/dist/chunk-FBOYSUYV.js +1280 -0
  8. package/dist/chunk-FR632TZX.js +1870 -0
  9. package/dist/chunk-HW4Z7YLJ.js +1242 -0
  10. package/dist/chunk-HWW62IFH.js +5424 -0
  11. package/dist/chunk-I2GZXRH4.js +4790 -0
  12. package/dist/chunk-JQZE3OK4.js +1255 -0
  13. package/dist/chunk-KF7JNK2F.js +1864 -0
  14. package/dist/chunk-KR3P2DYK.js +5655 -0
  15. package/dist/chunk-MO5DSFSW.js +2214 -0
  16. package/dist/chunk-MQAW33RJ.js +5530 -0
  17. package/dist/chunk-N4WUWZZX.js +2833 -0
  18. package/dist/chunk-NRJV7I4C.js +1331 -0
  19. package/dist/chunk-NXG52532.js +2230 -0
  20. package/dist/chunk-PVXF67CN.js +1278 -0
  21. package/dist/chunk-QSB6DHIF.js +5429 -0
  22. package/dist/chunk-QYWJT7HR.js +5837 -0
  23. package/dist/chunk-SWBRCMW7.js +7466 -0
  24. package/dist/chunk-TAT3ULSV.js +2214 -0
  25. package/dist/chunk-TTDP5JUM.js +2228 -0
  26. package/dist/chunk-UAGP4VPG.js +1739 -0
  27. package/dist/chunk-WIG6SY7A.js +1183 -0
  28. package/dist/chunk-YJ2K5N2R.js +6187 -0
  29. package/dist/index-3Lr_vKBd.d.cts +2810 -0
  30. package/dist/index-3Lr_vKBd.d.ts +2810 -0
  31. package/dist/index-7IPJn1yM.d.cts +1146 -0
  32. package/dist/index-7IPJn1yM.d.ts +1146 -0
  33. package/dist/index-B0xOv0V0.d.cts +3259 -0
  34. package/dist/index-B0xOv0V0.d.ts +3259 -0
  35. package/dist/index-B2m3zwg7.d.cts +1381 -0
  36. package/dist/index-B2m3zwg7.d.ts +1381 -0
  37. package/dist/index-B3sUrU_X.d.cts +1249 -0
  38. package/dist/index-B3sUrU_X.d.ts +1249 -0
  39. package/dist/index-B6wla7ZJ.d.cts +2751 -0
  40. package/dist/index-B6wla7ZJ.d.ts +2751 -0
  41. package/dist/index-BIv8RWWT.d.cts +1574 -0
  42. package/dist/index-BIv8RWWT.d.ts +1574 -0
  43. package/dist/index-BJv6hDHL.d.cts +3255 -0
  44. package/dist/index-BJv6hDHL.d.ts +3255 -0
  45. package/dist/index-BUCimS2e.d.cts +1393 -0
  46. package/dist/index-BUCimS2e.d.ts +1393 -0
  47. package/dist/index-Bw_nvNcG.d.cts +1275 -0
  48. package/dist/index-Bw_nvNcG.d.ts +1275 -0
  49. package/dist/index-ByG0gOtd.d.cts +1167 -0
  50. package/dist/index-ByG0gOtd.d.ts +1167 -0
  51. package/dist/index-CDGd2XXv.d.cts +2492 -0
  52. package/dist/index-CDGd2XXv.d.ts +2492 -0
  53. package/dist/index-CznAVeJ6.d.cts +1145 -0
  54. package/dist/index-CznAVeJ6.d.ts +1145 -0
  55. package/dist/index-DQD9IMh7.d.cts +2534 -0
  56. package/dist/index-DQD9IMh7.d.ts +2534 -0
  57. package/dist/index-Dl3qtJEI.d.cts +2178 -0
  58. package/dist/index-Dl3qtJEI.d.ts +2178 -0
  59. package/dist/index-DnE2A-Nz.d.cts +2603 -0
  60. package/dist/index-DnE2A-Nz.d.ts +2603 -0
  61. package/dist/index-DrOA6QmW.d.cts +2492 -0
  62. package/dist/index-DrOA6QmW.d.ts +2492 -0
  63. package/dist/index-Vpa3rPEM.d.cts +1402 -0
  64. package/dist/index-Vpa3rPEM.d.ts +1402 -0
  65. package/dist/index-jP6BomSd.d.cts +2640 -0
  66. package/dist/index-jP6BomSd.d.ts +2640 -0
  67. package/dist/index-wiGRwVyY.d.cts +3259 -0
  68. package/dist/index-wiGRwVyY.d.ts +3259 -0
  69. package/dist/index.cjs +7386 -0
  70. package/dist/index.d.cts +1 -0
  71. package/dist/index.d.ts +1 -0
  72. package/dist/index.js +263 -0
  73. package/dist/internal.cjs +7721 -0
  74. package/dist/internal.d.cts +704 -0
  75. package/dist/internal.d.ts +704 -0
  76. package/dist/internal.js +405 -0
  77. package/package.json +58 -0
@@ -0,0 +1,1870 @@
1
+ // src/types/clip.ts
2
+ function createClip(params) {
3
+ const clip = {
4
+ id: params.id,
5
+ assetId: params.assetId,
6
+ trackId: params.trackId,
7
+ timelineStart: params.timelineStart,
8
+ timelineEnd: params.timelineEnd,
9
+ mediaIn: params.mediaIn,
10
+ mediaOut: params.mediaOut
11
+ };
12
+ if (params.label !== void 0) {
13
+ clip.label = params.label;
14
+ }
15
+ if (params.metadata !== void 0) {
16
+ clip.metadata = params.metadata;
17
+ }
18
+ return clip;
19
+ }
20
+ function getClipDuration(clip) {
21
+ return clip.timelineEnd - clip.timelineStart;
22
+ }
23
+ function getClipMediaDuration(clip) {
24
+ return clip.mediaOut - clip.mediaIn;
25
+ }
26
+ function clipContainsFrame(clip, frame2) {
27
+ return frame2 >= clip.timelineStart && frame2 < clip.timelineEnd;
28
+ }
29
+ function clipsOverlap(clip1, clip2) {
30
+ return clip1.timelineStart < clip2.timelineEnd && clip2.timelineStart < clip1.timelineEnd;
31
+ }
32
+
33
+ // src/types/validation.ts
34
+ function validResult() {
35
+ return {
36
+ valid: true,
37
+ errors: []
38
+ };
39
+ }
40
+ function invalidResult(code, message, context) {
41
+ const error = { code, message };
42
+ if (context !== void 0) {
43
+ error.context = context;
44
+ }
45
+ return {
46
+ valid: false,
47
+ errors: [error]
48
+ };
49
+ }
50
+ function invalidResults(errors) {
51
+ return {
52
+ valid: false,
53
+ errors
54
+ };
55
+ }
56
+ function combineResults(...results) {
57
+ const allErrors = [];
58
+ for (const result of results) {
59
+ if (!result.valid) {
60
+ allErrors.push(...result.errors);
61
+ }
62
+ }
63
+ if (allErrors.length > 0) {
64
+ return invalidResults(allErrors);
65
+ }
66
+ return validResult();
67
+ }
68
+
69
+ // src/systems/asset-registry.ts
70
+ function registerAsset(state, asset) {
71
+ const newAssets = new Map(state.assets);
72
+ newAssets.set(asset.id, asset);
73
+ return {
74
+ ...state,
75
+ assets: newAssets
76
+ };
77
+ }
78
+ function getAsset(state, assetId) {
79
+ return state.assets.get(assetId);
80
+ }
81
+ function hasAsset(state, assetId) {
82
+ return state.assets.has(assetId);
83
+ }
84
+ function getAllAssets(state) {
85
+ return Array.from(state.assets.values());
86
+ }
87
+ function unregisterAsset(state, assetId) {
88
+ const newAssets = new Map(state.assets);
89
+ newAssets.delete(assetId);
90
+ return {
91
+ ...state,
92
+ assets: newAssets
93
+ };
94
+ }
95
+
96
+ // src/systems/validation.ts
97
+ function validateClip(state, clip) {
98
+ const errors = [];
99
+ const asset = getAsset(state, clip.assetId);
100
+ if (!asset) {
101
+ errors.push(invalidResult(
102
+ "ASSET_NOT_FOUND",
103
+ `Asset '${clip.assetId}' not found in registry`,
104
+ { clipId: clip.id, assetId: clip.assetId }
105
+ ));
106
+ return combineResults(...errors);
107
+ }
108
+ if (clip.timelineEnd <= clip.timelineStart) {
109
+ errors.push(invalidResult(
110
+ "INVALID_TIMELINE_BOUNDS",
111
+ `Clip timeline end (${clip.timelineEnd}) must be greater than start (${clip.timelineStart})`,
112
+ { clipId: clip.id, timelineStart: clip.timelineStart, timelineEnd: clip.timelineEnd }
113
+ ));
114
+ }
115
+ if (clip.mediaIn < 0) {
116
+ errors.push(invalidResult(
117
+ "INVALID_MEDIA_IN",
118
+ `Clip media in (${clip.mediaIn}) must be >= 0`,
119
+ { clipId: clip.id, mediaIn: clip.mediaIn }
120
+ ));
121
+ }
122
+ if (clip.mediaOut <= clip.mediaIn) {
123
+ errors.push(invalidResult(
124
+ "INVALID_MEDIA_BOUNDS",
125
+ `Clip media out (${clip.mediaOut}) must be greater than media in (${clip.mediaIn})`,
126
+ { clipId: clip.id, mediaIn: clip.mediaIn, mediaOut: clip.mediaOut }
127
+ ));
128
+ }
129
+ if (clip.mediaOut > asset.duration) {
130
+ errors.push(invalidResult(
131
+ "MEDIA_EXCEEDS_ASSET",
132
+ `Clip media out (${clip.mediaOut}) exceeds asset duration (${asset.duration})`,
133
+ { clipId: clip.id, mediaOut: clip.mediaOut, assetDuration: asset.duration }
134
+ ));
135
+ }
136
+ const timelineDuration = getClipDuration(clip);
137
+ const mediaDuration = getClipMediaDuration(clip);
138
+ if (timelineDuration !== mediaDuration) {
139
+ errors.push(invalidResult(
140
+ "DURATION_MISMATCH",
141
+ `Clip timeline duration (${timelineDuration}) must match media duration (${mediaDuration}) in Phase 1`,
142
+ { clipId: clip.id, timelineDuration, mediaDuration }
143
+ ));
144
+ }
145
+ return combineResults(...errors);
146
+ }
147
+ function validateTrack(state, track) {
148
+ const errors = [];
149
+ for (const clip of track.clips) {
150
+ const clipResult = validateClip(state, clip);
151
+ if (!clipResult.valid) {
152
+ errors.push(clipResult);
153
+ }
154
+ }
155
+ for (let i = 0; i < track.clips.length; i++) {
156
+ for (let j = i + 1; j < track.clips.length; j++) {
157
+ const clip1 = track.clips[i];
158
+ const clip2 = track.clips[j];
159
+ if (!clip1 || !clip2) {
160
+ continue;
161
+ }
162
+ if (clipsOverlap(clip1, clip2)) {
163
+ errors.push(invalidResult(
164
+ "CLIPS_OVERLAP",
165
+ `Clips '${clip1.id}' and '${clip2.id}' overlap on track '${track.id}'`,
166
+ {
167
+ trackId: track.id,
168
+ clip1Id: clip1.id,
169
+ clip2Id: clip2.id,
170
+ clip1Start: clip1.timelineStart,
171
+ clip1End: clip1.timelineEnd,
172
+ clip2Start: clip2.timelineStart,
173
+ clip2End: clip2.timelineEnd
174
+ }
175
+ ));
176
+ }
177
+ }
178
+ }
179
+ return combineResults(...errors);
180
+ }
181
+ function validateTimeline(state) {
182
+ const errors = [];
183
+ if (state.timeline.fps <= 0) {
184
+ errors.push(invalidResult(
185
+ "INVALID_FPS",
186
+ `Timeline FPS must be positive, got ${state.timeline.fps}`,
187
+ { fps: state.timeline.fps }
188
+ ));
189
+ }
190
+ if (state.timeline.duration <= 0) {
191
+ errors.push(invalidResult(
192
+ "INVALID_DURATION",
193
+ `Timeline duration must be positive, got ${state.timeline.duration}`,
194
+ { duration: state.timeline.duration }
195
+ ));
196
+ }
197
+ for (const track of state.timeline.tracks) {
198
+ const trackResult = validateTrack(state, track);
199
+ if (!trackResult.valid) {
200
+ errors.push(trackResult);
201
+ }
202
+ }
203
+ return combineResults(...errors);
204
+ }
205
+ function validateNoOverlap(track, clip) {
206
+ for (const existingClip of track.clips) {
207
+ if (existingClip.id === clip.id) {
208
+ continue;
209
+ }
210
+ if (clipsOverlap(existingClip, clip)) {
211
+ return invalidResult(
212
+ "CLIPS_OVERLAP",
213
+ `Clip '${clip.id}' would overlap with existing clip '${existingClip.id}' on track '${track.id}'`,
214
+ {
215
+ trackId: track.id,
216
+ newClipId: clip.id,
217
+ existingClipId: existingClip.id,
218
+ newClipStart: clip.timelineStart,
219
+ newClipEnd: clip.timelineEnd,
220
+ existingClipStart: existingClip.timelineStart,
221
+ existingClipEnd: existingClip.timelineEnd
222
+ }
223
+ );
224
+ }
225
+ }
226
+ return validResult();
227
+ }
228
+ function validateTrackTypeMatch(state, clip, targetTrack) {
229
+ const asset = getAsset(state, clip.assetId);
230
+ if (!asset) {
231
+ return invalidResult(
232
+ "ASSET_NOT_FOUND",
233
+ `Asset '${clip.assetId}' not found in registry`,
234
+ { clipId: clip.id, assetId: clip.assetId }
235
+ );
236
+ }
237
+ if (asset.type === "video" && targetTrack.type !== "video") {
238
+ return invalidResult(
239
+ "TRACK_TYPE_MISMATCH",
240
+ `Cannot place video clip '${clip.id}' on ${targetTrack.type} track '${targetTrack.id}'`,
241
+ {
242
+ clipId: clip.id,
243
+ assetType: asset.type,
244
+ trackType: targetTrack.type,
245
+ trackId: targetTrack.id
246
+ }
247
+ );
248
+ }
249
+ if (asset.type === "audio" && targetTrack.type !== "audio") {
250
+ return invalidResult(
251
+ "TRACK_TYPE_MISMATCH",
252
+ `Cannot place audio clip '${clip.id}' on ${targetTrack.type} track '${targetTrack.id}'`,
253
+ {
254
+ clipId: clip.id,
255
+ assetType: asset.type,
256
+ trackType: targetTrack.type,
257
+ trackId: targetTrack.id
258
+ }
259
+ );
260
+ }
261
+ if (asset.type === "image" && targetTrack.type !== "video") {
262
+ return invalidResult(
263
+ "TRACK_TYPE_MISMATCH",
264
+ `Cannot place image clip '${clip.id}' on ${targetTrack.type} track '${targetTrack.id}'`,
265
+ {
266
+ clipId: clip.id,
267
+ assetType: asset.type,
268
+ trackType: targetTrack.type,
269
+ trackId: targetTrack.id
270
+ }
271
+ );
272
+ }
273
+ return validResult();
274
+ }
275
+
276
+ // src/systems/queries.ts
277
+ function findClipById(state, clipId) {
278
+ for (const track of state.timeline.tracks) {
279
+ const clip = track.clips.find((c) => c.id === clipId);
280
+ if (clip) {
281
+ return clip;
282
+ }
283
+ }
284
+ return void 0;
285
+ }
286
+ function findTrackById(state, trackId) {
287
+ return state.timeline.tracks.find((t) => t.id === trackId);
288
+ }
289
+ function getClipsOnTrack(state, trackId) {
290
+ const track = findTrackById(state, trackId);
291
+ return track ? track.clips : [];
292
+ }
293
+ function getClipsAtFrame(state, frame2) {
294
+ const clips = [];
295
+ for (const track of state.timeline.tracks) {
296
+ for (const clip of track.clips) {
297
+ if (clipContainsFrame(clip, frame2)) {
298
+ clips.push(clip);
299
+ }
300
+ }
301
+ }
302
+ return clips;
303
+ }
304
+ function getClipsInRange(state, start, end) {
305
+ const clips = [];
306
+ for (const track of state.timeline.tracks) {
307
+ for (const clip of track.clips) {
308
+ if (clip.timelineStart < end && clip.timelineEnd > start) {
309
+ clips.push(clip);
310
+ }
311
+ }
312
+ }
313
+ return clips;
314
+ }
315
+ function getAllClips(state) {
316
+ const clips = [];
317
+ for (const track of state.timeline.tracks) {
318
+ clips.push(...track.clips);
319
+ }
320
+ return clips;
321
+ }
322
+ function getAllTracks(state) {
323
+ return state.timeline.tracks;
324
+ }
325
+ function findTrackIndex(state, trackId) {
326
+ return state.timeline.tracks.findIndex((t) => t.id === trackId);
327
+ }
328
+
329
+ // src/types/track.ts
330
+ function createTrack(params) {
331
+ const track = {
332
+ id: params.id,
333
+ name: params.name,
334
+ type: params.type,
335
+ clips: params.clips ?? [],
336
+ locked: params.locked ?? false,
337
+ muted: params.muted ?? false,
338
+ solo: params.solo ?? false,
339
+ height: params.height ?? 56
340
+ };
341
+ if (params.metadata !== void 0) {
342
+ track.metadata = params.metadata;
343
+ }
344
+ return track;
345
+ }
346
+ function sortTrackClips(track) {
347
+ return {
348
+ ...track,
349
+ clips: [...track.clips].sort((a, b) => a.timelineStart - b.timelineStart)
350
+ };
351
+ }
352
+
353
+ // src/operations/clip-operations.ts
354
+ function addClip(state, trackId, clip) {
355
+ const trackIndex = state.timeline.tracks.findIndex((t) => t.id === trackId);
356
+ if (trackIndex === -1) {
357
+ return state;
358
+ }
359
+ const track = state.timeline.tracks[trackIndex];
360
+ if (!track) {
361
+ return state;
362
+ }
363
+ const newTrack = sortTrackClips({
364
+ ...track,
365
+ clips: [...track.clips, clip]
366
+ });
367
+ const newTracks = [...state.timeline.tracks];
368
+ newTracks[trackIndex] = newTrack;
369
+ return {
370
+ ...state,
371
+ timeline: {
372
+ ...state.timeline,
373
+ tracks: newTracks
374
+ }
375
+ };
376
+ }
377
+ function removeClip(state, clipId) {
378
+ const newTracks = state.timeline.tracks.map((track) => ({
379
+ ...track,
380
+ clips: track.clips.filter((c) => c.id !== clipId)
381
+ }));
382
+ return {
383
+ ...state,
384
+ timeline: {
385
+ ...state.timeline,
386
+ tracks: newTracks
387
+ }
388
+ };
389
+ }
390
+ function moveClip(state, clipId, newStart) {
391
+ const clip = findClipById(state, clipId);
392
+ if (!clip) {
393
+ return state;
394
+ }
395
+ const duration = clip.timelineEnd - clip.timelineStart;
396
+ const newEnd = newStart + duration;
397
+ return updateClip(state, clipId, {
398
+ timelineStart: newStart,
399
+ timelineEnd: newEnd
400
+ });
401
+ }
402
+ function resizeClip(state, clipId, newStart, newEnd) {
403
+ const clip = findClipById(state, clipId);
404
+ if (!clip) {
405
+ return state;
406
+ }
407
+ const startDelta = newStart - clip.timelineStart;
408
+ const endDelta = newEnd - clip.timelineEnd;
409
+ let newMediaIn = clip.mediaIn;
410
+ let newMediaOut = clip.mediaOut;
411
+ if (startDelta !== 0) {
412
+ newMediaIn = clip.mediaIn + startDelta;
413
+ }
414
+ if (endDelta !== 0) {
415
+ newMediaOut = clip.mediaOut + endDelta;
416
+ }
417
+ return updateClip(state, clipId, {
418
+ timelineStart: newStart,
419
+ timelineEnd: newEnd,
420
+ mediaIn: newMediaIn,
421
+ mediaOut: newMediaOut
422
+ });
423
+ }
424
+ function trimClip(state, clipId, newMediaIn, newMediaOut) {
425
+ return updateClip(state, clipId, {
426
+ mediaIn: newMediaIn,
427
+ mediaOut: newMediaOut
428
+ });
429
+ }
430
+ function updateClip(state, clipId, updates) {
431
+ const newTracks = state.timeline.tracks.map((track) => {
432
+ const clipIndex = track.clips.findIndex((c) => c.id === clipId);
433
+ if (clipIndex === -1) {
434
+ return track;
435
+ }
436
+ const newClips = [...track.clips];
437
+ const existingClip = newClips[clipIndex];
438
+ if (!existingClip) {
439
+ return track;
440
+ }
441
+ newClips[clipIndex] = {
442
+ ...existingClip,
443
+ ...updates
444
+ };
445
+ return sortTrackClips({
446
+ ...track,
447
+ clips: newClips
448
+ });
449
+ });
450
+ return {
451
+ ...state,
452
+ timeline: {
453
+ ...state.timeline,
454
+ tracks: newTracks
455
+ }
456
+ };
457
+ }
458
+ function moveClipToTrack(state, clipId, targetTrackId) {
459
+ const clip = findClipById(state, clipId);
460
+ if (!clip) {
461
+ return state;
462
+ }
463
+ const targetTrack = findTrackById(state, targetTrackId);
464
+ if (!targetTrack) {
465
+ return state;
466
+ }
467
+ const validationResult = validateTrackTypeMatch(state, clip, targetTrack);
468
+ if (!validationResult.valid) {
469
+ return state;
470
+ }
471
+ if (clip.trackId === targetTrackId) {
472
+ return state;
473
+ }
474
+ let newState = removeClip(state, clipId);
475
+ const updatedClip = { ...clip, trackId: targetTrackId };
476
+ newState = addClip(newState, targetTrackId, updatedClip);
477
+ return newState;
478
+ }
479
+
480
+ // src/operations/track-operations.ts
481
+ function addTrack(state, track) {
482
+ return {
483
+ ...state,
484
+ timeline: {
485
+ ...state.timeline,
486
+ tracks: [...state.timeline.tracks, track]
487
+ }
488
+ };
489
+ }
490
+ function removeTrack(state, trackId) {
491
+ return {
492
+ ...state,
493
+ timeline: {
494
+ ...state.timeline,
495
+ tracks: state.timeline.tracks.filter((t) => t.id !== trackId)
496
+ }
497
+ };
498
+ }
499
+ function moveTrack(state, trackId, newIndex) {
500
+ const currentIndex = findTrackIndex(state, trackId);
501
+ if (currentIndex === -1) {
502
+ return state;
503
+ }
504
+ const newTracks = [...state.timeline.tracks];
505
+ const [track] = newTracks.splice(currentIndex, 1);
506
+ if (!track) {
507
+ return state;
508
+ }
509
+ newTracks.splice(newIndex, 0, track);
510
+ return {
511
+ ...state,
512
+ timeline: {
513
+ ...state.timeline,
514
+ tracks: newTracks
515
+ }
516
+ };
517
+ }
518
+ function updateTrack(state, trackId, updates) {
519
+ const trackIndex = findTrackIndex(state, trackId);
520
+ if (trackIndex === -1) {
521
+ return state;
522
+ }
523
+ const newTracks = [...state.timeline.tracks];
524
+ const existingTrack = newTracks[trackIndex];
525
+ if (!existingTrack) {
526
+ return state;
527
+ }
528
+ newTracks[trackIndex] = {
529
+ ...existingTrack,
530
+ ...updates
531
+ };
532
+ return {
533
+ ...state,
534
+ timeline: {
535
+ ...state.timeline,
536
+ tracks: newTracks
537
+ }
538
+ };
539
+ }
540
+ function toggleTrackMute(state, trackId) {
541
+ const trackIndex = findTrackIndex(state, trackId);
542
+ if (trackIndex === -1) {
543
+ return state;
544
+ }
545
+ const track = state.timeline.tracks[trackIndex];
546
+ if (!track) {
547
+ return state;
548
+ }
549
+ return updateTrack(state, trackId, { muted: !track.muted });
550
+ }
551
+ function toggleTrackLock(state, trackId) {
552
+ const trackIndex = findTrackIndex(state, trackId);
553
+ if (trackIndex === -1) {
554
+ return state;
555
+ }
556
+ const track = state.timeline.tracks[trackIndex];
557
+ if (!track) {
558
+ return state;
559
+ }
560
+ return updateTrack(state, trackId, { locked: !track.locked });
561
+ }
562
+ function toggleTrackSolo(state, trackId) {
563
+ const trackIndex = findTrackIndex(state, trackId);
564
+ if (trackIndex === -1) {
565
+ return state;
566
+ }
567
+ const track = state.timeline.tracks[trackIndex];
568
+ if (!track) {
569
+ return state;
570
+ }
571
+ return updateTrack(state, trackId, { solo: !track.solo });
572
+ }
573
+ function setTrackHeight(state, trackId, height) {
574
+ return updateTrack(state, trackId, { height: Math.max(40, Math.min(200, height)) });
575
+ }
576
+
577
+ // src/operations/timeline-operations.ts
578
+ function setTimelineDuration(state, duration) {
579
+ return {
580
+ ...state,
581
+ timeline: {
582
+ ...state.timeline,
583
+ duration
584
+ }
585
+ };
586
+ }
587
+ function setTimelineName(state, name) {
588
+ return {
589
+ ...state,
590
+ timeline: {
591
+ ...state.timeline,
592
+ name
593
+ }
594
+ };
595
+ }
596
+ function updateTimelineMetadata(state, metadata) {
597
+ return {
598
+ ...state,
599
+ timeline: {
600
+ ...state.timeline,
601
+ metadata: {
602
+ ...state.timeline.metadata,
603
+ ...metadata
604
+ }
605
+ }
606
+ };
607
+ }
608
+
609
+ // src/engine/transactions.ts
610
+ function beginTransaction(state) {
611
+ return {
612
+ initialState: state,
613
+ currentState: state,
614
+ operations: [],
615
+ finalized: false
616
+ };
617
+ }
618
+ function applyOperation(tx, operation) {
619
+ if (tx.finalized) {
620
+ throw new Error("Cannot apply operation to finalized transaction");
621
+ }
622
+ const newState = operation(tx.currentState);
623
+ return {
624
+ ...tx,
625
+ currentState: newState,
626
+ operations: [...tx.operations, operation]
627
+ };
628
+ }
629
+ function commitTransaction(tx) {
630
+ if (tx.finalized) {
631
+ throw new Error("Transaction already finalized");
632
+ }
633
+ tx.finalized = true;
634
+ return tx.currentState;
635
+ }
636
+ function rollbackTransaction(tx) {
637
+ if (tx.finalized) {
638
+ throw new Error("Transaction already finalized");
639
+ }
640
+ tx.finalized = true;
641
+ return tx.initialState;
642
+ }
643
+ function getOperationCount(tx) {
644
+ return tx.operations.length;
645
+ }
646
+
647
+ // src/operations/ripple.ts
648
+ function rippleDelete(state, clipId) {
649
+ const clip = findClipById(state, clipId);
650
+ if (!clip) {
651
+ throw new Error(`Clip not found: ${clipId}`);
652
+ }
653
+ const track = findTrackById(state, clip.trackId);
654
+ if (!track) {
655
+ throw new Error(`Track not found: ${clip.trackId}`);
656
+ }
657
+ const clipDuration = getClipDuration(clip);
658
+ const deleteEnd = clip.timelineEnd;
659
+ const clipsToShift = track.clips.filter(
660
+ (c) => c.id !== clipId && c.timelineStart >= deleteEnd
661
+ );
662
+ let tx = beginTransaction(state);
663
+ tx = applyOperation(tx, (s) => removeClip(s, clipId));
664
+ for (const clipToShift of clipsToShift) {
665
+ const newStart = clipToShift.timelineStart - clipDuration;
666
+ tx = applyOperation(tx, (s) => moveClip(s, clipToShift.id, newStart));
667
+ }
668
+ return commitTransaction(tx);
669
+ }
670
+ function rippleTrim(state, clipId, newEnd) {
671
+ const clip = findClipById(state, clipId);
672
+ if (!clip) {
673
+ throw new Error(`Clip not found: ${clipId}`);
674
+ }
675
+ const track = findTrackById(state, clip.trackId);
676
+ if (!track) {
677
+ throw new Error(`Track not found: ${clip.trackId}`);
678
+ }
679
+ if (newEnd <= clip.timelineStart) {
680
+ throw new Error("New end must be after clip start");
681
+ }
682
+ const delta = newEnd - clip.timelineEnd;
683
+ const clipsToShift = track.clips.filter(
684
+ (c) => c.id !== clipId && c.timelineStart >= clip.timelineEnd
685
+ );
686
+ let tx = beginTransaction(state);
687
+ tx = applyOperation(tx, (s) => resizeClip(s, clipId, clip.timelineStart, newEnd));
688
+ for (const clipToShift of clipsToShift) {
689
+ const newStart = clipToShift.timelineStart + delta;
690
+ tx = applyOperation(tx, (s) => moveClip(s, clipToShift.id, newStart));
691
+ }
692
+ return commitTransaction(tx);
693
+ }
694
+ function insertEdit(state, trackId, clip, atFrame) {
695
+ const track = findTrackById(state, trackId);
696
+ if (!track) {
697
+ throw new Error(`Track not found: ${trackId}`);
698
+ }
699
+ const clipDuration = getClipDuration(clip);
700
+ const clipsToShift = track.clips.filter((c) => c.timelineStart >= atFrame);
701
+ const adjustedClip = {
702
+ ...clip,
703
+ timelineStart: atFrame,
704
+ timelineEnd: atFrame + clipDuration
705
+ };
706
+ let tx = beginTransaction(state);
707
+ for (const clipToShift of clipsToShift) {
708
+ const newStart = clipToShift.timelineStart + clipDuration;
709
+ tx = applyOperation(tx, (s) => moveClip(s, clipToShift.id, newStart));
710
+ }
711
+ tx = applyOperation(tx, (s) => addClip(s, trackId, adjustedClip));
712
+ return commitTransaction(tx);
713
+ }
714
+ function rippleMove(state, clipId, newStart) {
715
+ const clip = findClipById(state, clipId);
716
+ if (!clip) {
717
+ throw new Error(`Clip not found: ${clipId}`);
718
+ }
719
+ const track = findTrackById(state, clip.trackId);
720
+ if (!track) {
721
+ throw new Error(`Track not found: ${clip.trackId}`);
722
+ }
723
+ const clipDuration = getClipDuration(clip);
724
+ const newEnd = newStart + clipDuration;
725
+ if (newStart < 0) {
726
+ throw new Error("Cannot move clip before timeline start (frame 0)");
727
+ }
728
+ if (newEnd > state.timeline.duration) {
729
+ throw new Error(`Cannot move clip beyond timeline duration (${state.timeline.duration} frames)`);
730
+ }
731
+ const originalStart = clip.timelineStart;
732
+ const originalEnd = clip.timelineEnd;
733
+ if (newStart === originalStart) {
734
+ return state;
735
+ }
736
+ let tx = beginTransaction(state);
737
+ if (newStart > originalStart) {
738
+ const afterSource = track.clips.filter((c) => c.id !== clipId && c.timelineStart >= originalEnd).sort((a, b) => a.timelineStart - b.timelineStart);
739
+ for (const other of afterSource) {
740
+ const s = other.timelineStart - clipDuration;
741
+ tx = applyOperation(tx, (st) => moveClip(st, other.id, s));
742
+ }
743
+ const anyClipBetween = afterSource.some((c) => c.timelineStart < newStart);
744
+ const collapsedDest = anyClipBetween ? newStart - clipDuration : newStart;
745
+ const currentTrack = tx.currentState.timeline.tracks.find((t) => t.id === track.id);
746
+ const atDest = currentTrack.clips.filter((c) => c.id !== clipId && c.timelineStart >= collapsedDest).sort((a, b) => b.timelineStart - a.timelineStart);
747
+ for (const other of atDest) {
748
+ const s = other.timelineStart + clipDuration;
749
+ tx = applyOperation(tx, (st) => moveClip(st, other.id, s));
750
+ }
751
+ tx = applyOperation(tx, (st) => moveClip(st, clipId, collapsedDest));
752
+ } else {
753
+ const afterSource = track.clips.filter((c) => c.id !== clipId && c.timelineStart >= originalEnd).sort((a, b) => a.timelineStart - b.timelineStart);
754
+ for (const other of afterSource) {
755
+ const s = other.timelineStart - clipDuration;
756
+ tx = applyOperation(tx, (st) => moveClip(st, other.id, s));
757
+ }
758
+ tx = applyOperation(tx, (st) => moveClip(st, clipId, newStart));
759
+ }
760
+ return commitTransaction(tx);
761
+ }
762
+ function insertMove(state, clipId, newStart) {
763
+ const clip = findClipById(state, clipId);
764
+ if (!clip) {
765
+ throw new Error(`Clip not found: ${clipId}`);
766
+ }
767
+ const track = findTrackById(state, clip.trackId);
768
+ if (!track) {
769
+ throw new Error(`Track not found: ${clip.trackId}`);
770
+ }
771
+ const clipDuration = getClipDuration(clip);
772
+ const newEnd = newStart + clipDuration;
773
+ if (newStart < 0) {
774
+ throw new Error("Cannot move clip before timeline start (frame 0)");
775
+ }
776
+ if (newEnd > state.timeline.duration) {
777
+ throw new Error(`Cannot move clip beyond timeline duration (${state.timeline.duration} frames)`);
778
+ }
779
+ if (newStart === clip.timelineStart) {
780
+ return state;
781
+ }
782
+ let tx = beginTransaction(state);
783
+ const clipsToShift = track.clips.filter(
784
+ (c) => c.id !== clipId && c.timelineStart >= newStart
785
+ );
786
+ for (const clipToShift of clipsToShift) {
787
+ const shiftedStart = clipToShift.timelineStart + clipDuration;
788
+ tx = applyOperation(tx, (s) => moveClip(s, clipToShift.id, shiftedStart));
789
+ }
790
+ tx = applyOperation(tx, (s) => moveClip(s, clipId, newStart));
791
+ return commitTransaction(tx);
792
+ }
793
+
794
+ // src/operations/marker-operations.ts
795
+ function addTimelineMarker(state, marker) {
796
+ return {
797
+ ...state,
798
+ markers: {
799
+ ...state.markers,
800
+ timeline: [...state.markers.timeline, marker]
801
+ }
802
+ };
803
+ }
804
+ function addClipMarker(state, marker) {
805
+ const clip = findClipById(state, marker.clipId);
806
+ if (!clip) {
807
+ throw new Error(`Clip not found: ${marker.clipId}`);
808
+ }
809
+ return {
810
+ ...state,
811
+ markers: {
812
+ ...state.markers,
813
+ clips: [...state.markers.clips, marker]
814
+ }
815
+ };
816
+ }
817
+ function addRegionMarker(state, marker) {
818
+ if (marker.startFrame >= marker.endFrame) {
819
+ throw new Error("Region marker start must be before end");
820
+ }
821
+ return {
822
+ ...state,
823
+ markers: {
824
+ ...state.markers,
825
+ regions: [...state.markers.regions, marker]
826
+ }
827
+ };
828
+ }
829
+ function removeMarker(state, markerId) {
830
+ return {
831
+ ...state,
832
+ markers: {
833
+ timeline: state.markers.timeline.filter((m) => m.id !== markerId),
834
+ clips: state.markers.clips.filter((m) => m.id !== markerId),
835
+ regions: state.markers.regions.filter((m) => m.id !== markerId)
836
+ }
837
+ };
838
+ }
839
+ function removeClipMarkers(state, clipId) {
840
+ return {
841
+ ...state,
842
+ markers: {
843
+ ...state.markers,
844
+ clips: state.markers.clips.filter((m) => m.clipId !== clipId)
845
+ }
846
+ };
847
+ }
848
+ function setWorkArea(state, workArea) {
849
+ if (workArea.startFrame >= workArea.endFrame) {
850
+ throw new Error("Work area start must be before end");
851
+ }
852
+ return {
853
+ ...state,
854
+ workArea
855
+ };
856
+ }
857
+ function clearWorkArea(state) {
858
+ const { workArea: _, ...stateWithoutWorkArea } = state;
859
+ return stateWithoutWorkArea;
860
+ }
861
+ function updateTimelineMarker(state, markerId, updates) {
862
+ return {
863
+ ...state,
864
+ markers: {
865
+ ...state.markers,
866
+ timeline: state.markers.timeline.map(
867
+ (m) => m.id === markerId ? { ...m, ...updates } : m
868
+ )
869
+ }
870
+ };
871
+ }
872
+ function updateRegionMarker(state, markerId, updates) {
873
+ return {
874
+ ...state,
875
+ markers: {
876
+ ...state.markers,
877
+ regions: state.markers.regions.map(
878
+ (m) => m.id === markerId ? { ...m, ...updates } : m
879
+ )
880
+ }
881
+ };
882
+ }
883
+
884
+ // src/engine/history.ts
885
+ function createHistory(initialState, limit = 50) {
886
+ return {
887
+ past: [],
888
+ present: initialState,
889
+ future: [],
890
+ limit
891
+ };
892
+ }
893
+ function pushHistory(history, newState) {
894
+ const newPast = [...history.past, history.present];
895
+ if (newPast.length > history.limit) {
896
+ newPast.shift();
897
+ }
898
+ return {
899
+ ...history,
900
+ past: newPast,
901
+ present: newState,
902
+ future: []
903
+ // Clear future on new action
904
+ };
905
+ }
906
+ function undo(history) {
907
+ if (history.past.length === 0) {
908
+ return history;
909
+ }
910
+ const newPast = [...history.past];
911
+ const previous = newPast.pop();
912
+ return {
913
+ ...history,
914
+ past: newPast,
915
+ present: previous,
916
+ future: [history.present, ...history.future]
917
+ };
918
+ }
919
+ function redo(history) {
920
+ if (history.future.length === 0) {
921
+ return history;
922
+ }
923
+ const newFuture = [...history.future];
924
+ const next = newFuture.shift();
925
+ return {
926
+ ...history,
927
+ past: [...history.past, history.present],
928
+ present: next,
929
+ future: newFuture
930
+ };
931
+ }
932
+ function canUndo(history) {
933
+ return history.past.length > 0;
934
+ }
935
+ function canRedo(history) {
936
+ return history.future.length > 0;
937
+ }
938
+ function getCurrentState(history) {
939
+ return history.present;
940
+ }
941
+
942
+ // src/engine/dispatcher.ts
943
+ function dispatch(history, operation) {
944
+ const currentState = getCurrentState(history);
945
+ let newState;
946
+ try {
947
+ newState = operation(currentState);
948
+ } catch (error) {
949
+ const errorMessage = error instanceof Error ? error.message : String(error);
950
+ return {
951
+ success: false,
952
+ history,
953
+ // Return unchanged history
954
+ errors: [{
955
+ code: "OPERATION_ERROR",
956
+ message: errorMessage
957
+ }]
958
+ };
959
+ }
960
+ const validationResult = validateTimeline(newState);
961
+ if (!validationResult.valid) {
962
+ return {
963
+ success: false,
964
+ history,
965
+ // Return unchanged history
966
+ errors: validationResult.errors
967
+ };
968
+ }
969
+ const newHistory = pushHistory(history, newState);
970
+ return {
971
+ success: true,
972
+ history: newHistory
973
+ };
974
+ }
975
+
976
+ // src/engine/timeline-engine.ts
977
+ var TimelineEngine = class {
978
+ history;
979
+ listeners = /* @__PURE__ */ new Set();
980
+ /**
981
+ * Create a new timeline engine
982
+ *
983
+ * @param initialState - Initial timeline state
984
+ * @param historyLimit - Maximum number of undo steps (default: 50)
985
+ */
986
+ constructor(initialState, historyLimit = 50) {
987
+ this.history = createHistory(initialState, historyLimit);
988
+ }
989
+ // ===== SUBSCRIPTION =====
990
+ /**
991
+ * Subscribe to state changes
992
+ *
993
+ * The listener will be called whenever the timeline state changes,
994
+ * with the new state passed as an argument.
995
+ * This is used by framework adapters (e.g., React) to trigger re-renders.
996
+ *
997
+ * @param listener - Function to call on state changes, receives new state
998
+ * @returns Unsubscribe function
999
+ *
1000
+ * @example
1001
+ * ```typescript
1002
+ * const unsubscribe = engine.subscribe((state) => {
1003
+ * console.log('State changed:', state);
1004
+ * });
1005
+ *
1006
+ * // Later...
1007
+ * unsubscribe();
1008
+ * ```
1009
+ */
1010
+ subscribe(listener) {
1011
+ this.listeners.add(listener);
1012
+ return () => {
1013
+ this.listeners.delete(listener);
1014
+ };
1015
+ }
1016
+ /**
1017
+ * Notify all subscribers of a state change
1018
+ *
1019
+ * This is called internally after any operation that modifies state.
1020
+ * Framework adapters use this to trigger re-renders.
1021
+ */
1022
+ notify() {
1023
+ const state = this.getState();
1024
+ this.listeners.forEach((listener) => listener(state));
1025
+ }
1026
+ // ===== STATE ACCESS =====
1027
+ /**
1028
+ * Get the current timeline state
1029
+ *
1030
+ * @returns Current timeline state
1031
+ */
1032
+ getState() {
1033
+ return getCurrentState(this.history);
1034
+ }
1035
+ // ===== ASSET OPERATIONS =====
1036
+ /**
1037
+ * Register an asset
1038
+ *
1039
+ * @param asset - Asset to register
1040
+ * @returns Dispatch result
1041
+ */
1042
+ registerAsset(asset) {
1043
+ const result = dispatch(
1044
+ this.history,
1045
+ (state) => registerAsset(state, asset)
1046
+ );
1047
+ if (result.success) {
1048
+ this.history = result.history;
1049
+ this.notify();
1050
+ }
1051
+ return result;
1052
+ }
1053
+ /**
1054
+ * Get an asset by ID
1055
+ *
1056
+ * @param assetId - Asset ID
1057
+ * @returns The asset, or undefined if not found
1058
+ */
1059
+ getAsset(assetId) {
1060
+ return getAsset(this.getState(), assetId);
1061
+ }
1062
+ // ===== CLIP OPERATIONS =====
1063
+ /**
1064
+ * Add a clip to a track
1065
+ *
1066
+ * @param trackId - ID of the track to add to
1067
+ * @param clip - Clip to add
1068
+ * @returns Dispatch result
1069
+ */
1070
+ addClip(trackId, clip) {
1071
+ const result = dispatch(
1072
+ this.history,
1073
+ (state) => addClip(state, trackId, clip)
1074
+ );
1075
+ if (result.success) {
1076
+ this.history = result.history;
1077
+ this.notify();
1078
+ }
1079
+ return result;
1080
+ }
1081
+ /**
1082
+ * Remove a clip
1083
+ *
1084
+ * @param clipId - ID of the clip to remove
1085
+ * @returns Dispatch result
1086
+ */
1087
+ removeClip(clipId) {
1088
+ const result = dispatch(
1089
+ this.history,
1090
+ (state) => removeClip(state, clipId)
1091
+ );
1092
+ if (result.success) {
1093
+ this.history = result.history;
1094
+ this.notify();
1095
+ }
1096
+ return result;
1097
+ }
1098
+ /**
1099
+ * Move a clip to a new timeline position
1100
+ *
1101
+ * @param clipId - ID of the clip to move
1102
+ * @param newStart - New timeline start frame
1103
+ * @returns Dispatch result
1104
+ */
1105
+ moveClip(clipId, newStart) {
1106
+ const result = dispatch(
1107
+ this.history,
1108
+ (state) => moveClip(state, clipId, newStart)
1109
+ );
1110
+ if (result.success) {
1111
+ this.history = result.history;
1112
+ this.notify();
1113
+ }
1114
+ return result;
1115
+ }
1116
+ /**
1117
+ * Resize a clip
1118
+ *
1119
+ * @param clipId - ID of the clip to resize
1120
+ * @param newStart - New timeline start frame
1121
+ * @param newEnd - New timeline end frame
1122
+ * @returns Dispatch result
1123
+ */
1124
+ resizeClip(clipId, newStart, newEnd) {
1125
+ const result = dispatch(
1126
+ this.history,
1127
+ (state) => resizeClip(state, clipId, newStart, newEnd)
1128
+ );
1129
+ if (result.success) {
1130
+ this.history = result.history;
1131
+ this.notify();
1132
+ }
1133
+ return result;
1134
+ }
1135
+ /**
1136
+ * Trim a clip (change media bounds)
1137
+ *
1138
+ * @param clipId - ID of the clip to trim
1139
+ * @param newMediaIn - New media in frame
1140
+ * @param newMediaOut - New media out frame
1141
+ * @returns Dispatch result
1142
+ */
1143
+ trimClip(clipId, newMediaIn, newMediaOut) {
1144
+ const result = dispatch(
1145
+ this.history,
1146
+ (state) => trimClip(state, clipId, newMediaIn, newMediaOut)
1147
+ );
1148
+ if (result.success) {
1149
+ this.history = result.history;
1150
+ this.notify();
1151
+ }
1152
+ return result;
1153
+ }
1154
+ /**
1155
+ * Move a clip to a different track
1156
+ *
1157
+ * @param clipId - ID of the clip to move
1158
+ * @param targetTrackId - ID of the target track
1159
+ * @returns Dispatch result
1160
+ */
1161
+ moveClipToTrack(clipId, targetTrackId) {
1162
+ const result = dispatch(
1163
+ this.history,
1164
+ (state) => moveClipToTrack(state, clipId, targetTrackId)
1165
+ );
1166
+ if (result.success) {
1167
+ this.history = result.history;
1168
+ this.notify();
1169
+ }
1170
+ return result;
1171
+ }
1172
+ // ===== TRACK OPERATIONS =====
1173
+ /**
1174
+ * Add a track
1175
+ *
1176
+ * @param track - Track to add
1177
+ * @returns Dispatch result
1178
+ */
1179
+ addTrack(track) {
1180
+ const result = dispatch(
1181
+ this.history,
1182
+ (state) => addTrack(state, track)
1183
+ );
1184
+ if (result.success) {
1185
+ this.history = result.history;
1186
+ this.notify();
1187
+ }
1188
+ return result;
1189
+ }
1190
+ /**
1191
+ * Remove a track
1192
+ *
1193
+ * @param trackId - ID of the track to remove
1194
+ * @returns Dispatch result
1195
+ */
1196
+ removeTrack(trackId) {
1197
+ const result = dispatch(
1198
+ this.history,
1199
+ (state) => removeTrack(state, trackId)
1200
+ );
1201
+ if (result.success) {
1202
+ this.history = result.history;
1203
+ this.notify();
1204
+ }
1205
+ return result;
1206
+ }
1207
+ /**
1208
+ * Move a track to a new position
1209
+ *
1210
+ * @param trackId - ID of the track to move
1211
+ * @param newIndex - New index position
1212
+ * @returns Dispatch result
1213
+ */
1214
+ moveTrack(trackId, newIndex) {
1215
+ const result = dispatch(
1216
+ this.history,
1217
+ (state) => moveTrack(state, trackId, newIndex)
1218
+ );
1219
+ if (result.success) {
1220
+ this.history = result.history;
1221
+ this.notify();
1222
+ }
1223
+ return result;
1224
+ }
1225
+ /**
1226
+ * Toggle track mute
1227
+ *
1228
+ * @param trackId - ID of the track
1229
+ * @returns Dispatch result
1230
+ */
1231
+ toggleTrackMute(trackId) {
1232
+ const result = dispatch(
1233
+ this.history,
1234
+ (state) => toggleTrackMute(state, trackId)
1235
+ );
1236
+ if (result.success) {
1237
+ this.history = result.history;
1238
+ this.notify();
1239
+ }
1240
+ return result;
1241
+ }
1242
+ /**
1243
+ * Toggle track lock
1244
+ *
1245
+ * @param trackId - ID of the track
1246
+ * @returns Dispatch result
1247
+ */
1248
+ toggleTrackLock(trackId) {
1249
+ const result = dispatch(
1250
+ this.history,
1251
+ (state) => toggleTrackLock(state, trackId)
1252
+ );
1253
+ if (result.success) {
1254
+ this.history = result.history;
1255
+ this.notify();
1256
+ }
1257
+ return result;
1258
+ }
1259
+ /**
1260
+ * Toggle track solo
1261
+ *
1262
+ * @param trackId - ID of the track
1263
+ * @returns Dispatch result
1264
+ */
1265
+ toggleTrackSolo(trackId) {
1266
+ const result = dispatch(
1267
+ this.history,
1268
+ (state) => toggleTrackSolo(state, trackId)
1269
+ );
1270
+ if (result.success) {
1271
+ this.history = result.history;
1272
+ this.notify();
1273
+ }
1274
+ return result;
1275
+ }
1276
+ /**
1277
+ * Set track height
1278
+ *
1279
+ * @param trackId - ID of the track
1280
+ * @param height - New height in pixels
1281
+ * @returns Dispatch result
1282
+ */
1283
+ setTrackHeight(trackId, height) {
1284
+ const result = dispatch(
1285
+ this.history,
1286
+ (state) => setTrackHeight(state, trackId, height)
1287
+ );
1288
+ if (result.success) {
1289
+ this.history = result.history;
1290
+ this.notify();
1291
+ }
1292
+ return result;
1293
+ }
1294
+ // ===== TIMELINE OPERATIONS =====
1295
+ /**
1296
+ * Set timeline duration
1297
+ *
1298
+ * @param duration - New duration in frames
1299
+ * @returns Dispatch result
1300
+ */
1301
+ setTimelineDuration(duration) {
1302
+ const result = dispatch(
1303
+ this.history,
1304
+ (state) => setTimelineDuration(state, duration)
1305
+ );
1306
+ if (result.success) {
1307
+ this.history = result.history;
1308
+ this.notify();
1309
+ }
1310
+ return result;
1311
+ }
1312
+ /**
1313
+ * Set timeline name
1314
+ *
1315
+ * @param name - New timeline name
1316
+ * @returns Dispatch result
1317
+ */
1318
+ setTimelineName(name) {
1319
+ const result = dispatch(
1320
+ this.history,
1321
+ (state) => setTimelineName(state, name)
1322
+ );
1323
+ if (result.success) {
1324
+ this.history = result.history;
1325
+ this.notify();
1326
+ }
1327
+ return result;
1328
+ }
1329
+ // ===== HISTORY OPERATIONS =====
1330
+ /**
1331
+ * Undo the last action
1332
+ *
1333
+ * @returns true if undo was performed
1334
+ */
1335
+ undo() {
1336
+ if (!this.canUndo()) {
1337
+ return false;
1338
+ }
1339
+ this.history = undo(this.history);
1340
+ this.notify();
1341
+ return true;
1342
+ }
1343
+ /**
1344
+ * Redo the last undone action
1345
+ *
1346
+ * @returns true if redo was performed
1347
+ */
1348
+ redo() {
1349
+ if (!this.canRedo()) {
1350
+ return false;
1351
+ }
1352
+ this.history = redo(this.history);
1353
+ this.notify();
1354
+ return true;
1355
+ }
1356
+ /**
1357
+ * Check if undo is available
1358
+ *
1359
+ * @returns true if undo is available
1360
+ */
1361
+ canUndo() {
1362
+ return canUndo(this.history);
1363
+ }
1364
+ /**
1365
+ * Check if redo is available
1366
+ *
1367
+ * @returns true if redo is available
1368
+ */
1369
+ canRedo() {
1370
+ return canRedo(this.history);
1371
+ }
1372
+ // ===== QUERY OPERATIONS =====
1373
+ /**
1374
+ * Find a clip by ID
1375
+ *
1376
+ * @param clipId - Clip ID
1377
+ * @returns The clip, or undefined if not found
1378
+ */
1379
+ findClipById(clipId) {
1380
+ return findClipById(this.getState(), clipId);
1381
+ }
1382
+ /**
1383
+ * Find a track by ID
1384
+ *
1385
+ * @param trackId - Track ID
1386
+ * @returns The track, or undefined if not found
1387
+ */
1388
+ findTrackById(trackId) {
1389
+ return findTrackById(this.getState(), trackId);
1390
+ }
1391
+ /**
1392
+ * Get all clips on a track
1393
+ *
1394
+ * @param trackId - Track ID
1395
+ * @returns Array of clips on the track
1396
+ */
1397
+ getClipsOnTrack(trackId) {
1398
+ return getClipsOnTrack(this.getState(), trackId);
1399
+ }
1400
+ /**
1401
+ * Get all clips at a specific frame
1402
+ *
1403
+ * @param frame - Frame to check
1404
+ * @returns Array of clips at that frame
1405
+ */
1406
+ getClipsAtFrame(frame2) {
1407
+ return getClipsAtFrame(this.getState(), frame2);
1408
+ }
1409
+ /**
1410
+ * Get all clips in a frame range
1411
+ *
1412
+ * @param start - Start frame
1413
+ * @param end - End frame
1414
+ * @returns Array of clips in the range
1415
+ */
1416
+ getClipsInRange(start, end) {
1417
+ return getClipsInRange(this.getState(), start, end);
1418
+ }
1419
+ /**
1420
+ * Get all clips in the timeline
1421
+ *
1422
+ * @returns Array of all clips
1423
+ */
1424
+ getAllClips() {
1425
+ return getAllClips(this.getState());
1426
+ }
1427
+ /**
1428
+ * Get all tracks in the timeline
1429
+ *
1430
+ * @returns Array of all tracks
1431
+ */
1432
+ getAllTracks() {
1433
+ return getAllTracks(this.getState());
1434
+ }
1435
+ // ===== RIPPLE OPERATIONS =====
1436
+ /**
1437
+ * Ripple delete - delete clip and shift subsequent clips left
1438
+ *
1439
+ * @param clipId - ID of the clip to delete
1440
+ * @returns Dispatch result
1441
+ */
1442
+ rippleDelete(clipId) {
1443
+ const result = dispatch(
1444
+ this.history,
1445
+ (state) => rippleDelete(state, clipId)
1446
+ );
1447
+ if (result.success) {
1448
+ this.history = result.history;
1449
+ this.notify();
1450
+ }
1451
+ return result;
1452
+ }
1453
+ /**
1454
+ * Ripple trim - trim clip end and shift subsequent clips
1455
+ *
1456
+ * @param clipId - ID of the clip to trim
1457
+ * @param newEnd - New end frame for the clip
1458
+ * @returns Dispatch result
1459
+ */
1460
+ rippleTrim(clipId, newEnd) {
1461
+ const result = dispatch(
1462
+ this.history,
1463
+ (state) => rippleTrim(state, clipId, newEnd)
1464
+ );
1465
+ if (result.success) {
1466
+ this.history = result.history;
1467
+ this.notify();
1468
+ }
1469
+ return result;
1470
+ }
1471
+ /**
1472
+ * Insert edit - insert clip and shift subsequent clips right
1473
+ *
1474
+ * @param trackId - ID of the track to insert into
1475
+ * @param clip - Clip to insert
1476
+ * @param atFrame - Frame to insert at
1477
+ * @returns Dispatch result
1478
+ */
1479
+ insertEdit(trackId, clip, atFrame) {
1480
+ const result = dispatch(
1481
+ this.history,
1482
+ (state) => insertEdit(state, trackId, clip, atFrame)
1483
+ );
1484
+ if (result.success) {
1485
+ this.history = result.history;
1486
+ this.notify();
1487
+ }
1488
+ return result;
1489
+ }
1490
+ /**
1491
+ * Ripple move - move clip and shift surrounding clips to accommodate
1492
+ *
1493
+ * This moves a clip to a new position while maintaining timeline continuity:
1494
+ * - Closes the gap at the source position
1495
+ * - Makes space at the destination position
1496
+ * - All operations are atomic (single undo entry)
1497
+ *
1498
+ * @param clipId - ID of the clip to move
1499
+ * @param newStart - New start frame for the clip
1500
+ * @returns Dispatch result
1501
+ */
1502
+ rippleMove(clipId, newStart) {
1503
+ const result = dispatch(
1504
+ this.history,
1505
+ (state) => rippleMove(state, clipId, newStart)
1506
+ );
1507
+ if (result.success) {
1508
+ this.history = result.history;
1509
+ this.notify();
1510
+ } else if (result.errors?.[0]?.code === "OPERATION_ERROR") {
1511
+ throw new Error(result.errors[0].message);
1512
+ }
1513
+ return result;
1514
+ }
1515
+ /**
1516
+ * Insert move - move clip and shift destination clips right
1517
+ *
1518
+ * This moves a clip to a new position without closing the gap at source:
1519
+ * - Leaves gap at the source position
1520
+ * - Pushes all clips at destination right to make space
1521
+ * - All operations are atomic (single undo entry)
1522
+ *
1523
+ * @param clipId - ID of the clip to move
1524
+ * @param newStart - New start frame for the clip
1525
+ * @returns Dispatch result
1526
+ */
1527
+ insertMove(clipId, newStart) {
1528
+ const result = dispatch(
1529
+ this.history,
1530
+ (state) => insertMove(state, clipId, newStart)
1531
+ );
1532
+ if (result.success) {
1533
+ this.history = result.history;
1534
+ this.notify();
1535
+ } else if (result.errors?.[0]?.code === "OPERATION_ERROR") {
1536
+ throw new Error(result.errors[0].message);
1537
+ }
1538
+ return result;
1539
+ }
1540
+ // ===== MARKER OPERATIONS =====
1541
+ /**
1542
+ * Add a timeline marker
1543
+ *
1544
+ * @param marker - Timeline marker to add
1545
+ * @returns Dispatch result
1546
+ */
1547
+ addTimelineMarker(marker) {
1548
+ const result = dispatch(
1549
+ this.history,
1550
+ (state) => addTimelineMarker(state, marker)
1551
+ );
1552
+ if (result.success) {
1553
+ this.history = result.history;
1554
+ this.notify();
1555
+ }
1556
+ return result;
1557
+ }
1558
+ /**
1559
+ * Add a clip marker
1560
+ *
1561
+ * @param marker - Clip marker to add
1562
+ * @returns Dispatch result
1563
+ */
1564
+ addClipMarker(marker) {
1565
+ const result = dispatch(
1566
+ this.history,
1567
+ (state) => addClipMarker(state, marker)
1568
+ );
1569
+ if (result.success) {
1570
+ this.history = result.history;
1571
+ this.notify();
1572
+ }
1573
+ return result;
1574
+ }
1575
+ /**
1576
+ * Add a region marker
1577
+ *
1578
+ * @param marker - Region marker to add
1579
+ * @returns Dispatch result
1580
+ */
1581
+ addRegionMarker(marker) {
1582
+ const result = dispatch(
1583
+ this.history,
1584
+ (state) => addRegionMarker(state, marker)
1585
+ );
1586
+ if (result.success) {
1587
+ this.history = result.history;
1588
+ this.notify();
1589
+ }
1590
+ return result;
1591
+ }
1592
+ /**
1593
+ * Remove a marker by ID
1594
+ *
1595
+ * @param markerId - ID of the marker to remove
1596
+ * @returns Dispatch result
1597
+ */
1598
+ removeMarker(markerId) {
1599
+ const result = dispatch(
1600
+ this.history,
1601
+ (state) => removeMarker(state, markerId)
1602
+ );
1603
+ if (result.success) {
1604
+ this.history = result.history;
1605
+ this.notify();
1606
+ }
1607
+ return result;
1608
+ }
1609
+ /**
1610
+ * Update a timeline marker
1611
+ *
1612
+ * @param markerId - ID of the marker to update
1613
+ * @param updates - Partial marker updates
1614
+ * @returns Dispatch result
1615
+ */
1616
+ updateTimelineMarker(markerId, updates) {
1617
+ const result = dispatch(
1618
+ this.history,
1619
+ (state) => updateTimelineMarker(state, markerId, updates)
1620
+ );
1621
+ if (result.success) {
1622
+ this.history = result.history;
1623
+ this.notify();
1624
+ }
1625
+ return result;
1626
+ }
1627
+ /**
1628
+ * Update a region marker
1629
+ *
1630
+ * @param markerId - ID of the marker to update
1631
+ * @param updates - Partial marker updates
1632
+ * @returns Dispatch result
1633
+ */
1634
+ updateRegionMarker(markerId, updates) {
1635
+ const result = dispatch(
1636
+ this.history,
1637
+ (state) => updateRegionMarker(state, markerId, updates)
1638
+ );
1639
+ if (result.success) {
1640
+ this.history = result.history;
1641
+ this.notify();
1642
+ }
1643
+ return result;
1644
+ }
1645
+ // ===== WORK AREA OPERATIONS =====
1646
+ /**
1647
+ * Set work area
1648
+ *
1649
+ * @param start - Start frame
1650
+ * @param end - End frame
1651
+ * @returns Dispatch result
1652
+ */
1653
+ setWorkArea(start, end) {
1654
+ const result = dispatch(
1655
+ this.history,
1656
+ (state) => setWorkArea(state, { startFrame: start, endFrame: end })
1657
+ );
1658
+ if (result.success) {
1659
+ this.history = result.history;
1660
+ this.notify();
1661
+ }
1662
+ return result;
1663
+ }
1664
+ /**
1665
+ * Clear work area
1666
+ *
1667
+ * @returns Dispatch result
1668
+ */
1669
+ clearWorkArea() {
1670
+ const result = dispatch(
1671
+ this.history,
1672
+ (state) => clearWorkArea(state)
1673
+ );
1674
+ if (result.success) {
1675
+ this.history = result.history;
1676
+ this.notify();
1677
+ }
1678
+ return result;
1679
+ }
1680
+ };
1681
+
1682
+ // src/types/timeline.ts
1683
+ function createTimeline(params) {
1684
+ const timeline = {
1685
+ id: params.id,
1686
+ name: params.name,
1687
+ fps: params.fps,
1688
+ duration: params.duration,
1689
+ tracks: params.tracks ?? []
1690
+ };
1691
+ if (params.metadata !== void 0) {
1692
+ timeline.metadata = params.metadata;
1693
+ }
1694
+ return timeline;
1695
+ }
1696
+
1697
+ // src/types/asset.ts
1698
+ function createAsset(params) {
1699
+ const asset = {
1700
+ id: params.id,
1701
+ type: params.type,
1702
+ duration: params.duration,
1703
+ sourceUrl: params.sourceUrl
1704
+ };
1705
+ if (params.metadata !== void 0) {
1706
+ asset.metadata = params.metadata;
1707
+ }
1708
+ return asset;
1709
+ }
1710
+
1711
+ // src/types/state.ts
1712
+ function createTimelineState(params) {
1713
+ const state = {
1714
+ timeline: params.timeline,
1715
+ assets: params.assets ?? /* @__PURE__ */ new Map(),
1716
+ linkGroups: params.linkGroups ?? /* @__PURE__ */ new Map(),
1717
+ groups: params.groups ?? /* @__PURE__ */ new Map(),
1718
+ markers: params.markers ?? {
1719
+ timeline: [],
1720
+ clips: [],
1721
+ regions: []
1722
+ }
1723
+ };
1724
+ if (params.workArea !== void 0) {
1725
+ state.workArea = params.workArea;
1726
+ }
1727
+ return state;
1728
+ }
1729
+
1730
+ // src/types/frame.ts
1731
+ function frame(value) {
1732
+ const rounded = Math.round(value);
1733
+ if (rounded < 0) {
1734
+ throw new Error(`Frame value must be non-negative, got: ${value}`);
1735
+ }
1736
+ return rounded;
1737
+ }
1738
+ function frameRate(value) {
1739
+ if (value <= 0) {
1740
+ throw new Error(`FrameRate must be positive, got: ${value}`);
1741
+ }
1742
+ return value;
1743
+ }
1744
+ function isValidFrame(value) {
1745
+ return Number.isInteger(value) && value >= 0;
1746
+ }
1747
+ function isValidFrameRate(value) {
1748
+ return value > 0;
1749
+ }
1750
+
1751
+ // src/utils/frame.ts
1752
+ function framesToSeconds(frames, fps) {
1753
+ return frames / fps;
1754
+ }
1755
+ function secondsToFrames(seconds, fps) {
1756
+ return frame(seconds * fps);
1757
+ }
1758
+ function framesToTimecode(frames, fps) {
1759
+ const totalFrames = frames;
1760
+ const framesPart = totalFrames % fps;
1761
+ const totalSeconds = Math.floor(totalFrames / fps);
1762
+ const secondsPart = totalSeconds % 60;
1763
+ const totalMinutes = Math.floor(totalSeconds / 60);
1764
+ const minutesPart = totalMinutes % 60;
1765
+ const hoursPart = Math.floor(totalMinutes / 60);
1766
+ return `${pad(hoursPart)}:${pad(minutesPart)}:${pad(secondsPart)}:${pad(framesPart)}`;
1767
+ }
1768
+ function framesToMinutesSeconds(frames, fps) {
1769
+ const totalSeconds = Math.floor(frames / fps);
1770
+ const minutes = Math.floor(totalSeconds / 60);
1771
+ const seconds = totalSeconds % 60;
1772
+ return `${minutes}:${pad(seconds)}`;
1773
+ }
1774
+ function clampFrame(value, min, max) {
1775
+ return frame(Math.max(min, Math.min(max, value)));
1776
+ }
1777
+ function addFrames(a, b) {
1778
+ return frame(a + b);
1779
+ }
1780
+ function subtractFrames(a, b) {
1781
+ return frame(Math.max(0, a - b));
1782
+ }
1783
+ function frameDuration(start, end) {
1784
+ return frame(end - start);
1785
+ }
1786
+ function pad(num, width = 2) {
1787
+ return num.toString().padStart(width, "0");
1788
+ }
1789
+
1790
+ export {
1791
+ createClip,
1792
+ getClipDuration,
1793
+ getClipMediaDuration,
1794
+ clipContainsFrame,
1795
+ clipsOverlap,
1796
+ validResult,
1797
+ invalidResult,
1798
+ invalidResults,
1799
+ combineResults,
1800
+ registerAsset,
1801
+ getAsset,
1802
+ hasAsset,
1803
+ getAllAssets,
1804
+ unregisterAsset,
1805
+ validateClip,
1806
+ validateTrack,
1807
+ validateTimeline,
1808
+ validateNoOverlap,
1809
+ findClipById,
1810
+ findTrackById,
1811
+ getClipsOnTrack,
1812
+ getClipsAtFrame,
1813
+ getClipsInRange,
1814
+ getAllClips,
1815
+ getAllTracks,
1816
+ findTrackIndex,
1817
+ createTrack,
1818
+ sortTrackClips,
1819
+ addClip,
1820
+ removeClip,
1821
+ moveClip,
1822
+ resizeClip,
1823
+ trimClip,
1824
+ updateClip,
1825
+ moveClipToTrack,
1826
+ addTrack,
1827
+ removeTrack,
1828
+ moveTrack,
1829
+ updateTrack,
1830
+ toggleTrackMute,
1831
+ toggleTrackLock,
1832
+ setTimelineDuration,
1833
+ setTimelineName,
1834
+ updateTimelineMetadata,
1835
+ beginTransaction,
1836
+ applyOperation,
1837
+ commitTransaction,
1838
+ rollbackTransaction,
1839
+ getOperationCount,
1840
+ rippleDelete,
1841
+ rippleTrim,
1842
+ insertEdit,
1843
+ rippleMove,
1844
+ insertMove,
1845
+ addTimelineMarker,
1846
+ addClipMarker,
1847
+ addRegionMarker,
1848
+ removeMarker,
1849
+ removeClipMarkers,
1850
+ setWorkArea,
1851
+ clearWorkArea,
1852
+ updateTimelineMarker,
1853
+ updateRegionMarker,
1854
+ TimelineEngine,
1855
+ createTimeline,
1856
+ createAsset,
1857
+ createTimelineState,
1858
+ frame,
1859
+ frameRate,
1860
+ isValidFrame,
1861
+ isValidFrameRate,
1862
+ framesToSeconds,
1863
+ secondsToFrames,
1864
+ framesToTimecode,
1865
+ framesToMinutesSeconds,
1866
+ clampFrame,
1867
+ addFrames,
1868
+ subtractFrames,
1869
+ frameDuration
1870
+ };