@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,1280 @@
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
+ };
339
+ if (params.metadata !== void 0) {
340
+ track.metadata = params.metadata;
341
+ }
342
+ return track;
343
+ }
344
+ function sortTrackClips(track) {
345
+ return {
346
+ ...track,
347
+ clips: [...track.clips].sort((a, b) => a.timelineStart - b.timelineStart)
348
+ };
349
+ }
350
+
351
+ // src/operations/clip-operations.ts
352
+ function addClip(state, trackId, clip) {
353
+ const trackIndex = state.timeline.tracks.findIndex((t) => t.id === trackId);
354
+ if (trackIndex === -1) {
355
+ return state;
356
+ }
357
+ const track = state.timeline.tracks[trackIndex];
358
+ if (!track) {
359
+ return state;
360
+ }
361
+ const newTrack = sortTrackClips({
362
+ ...track,
363
+ clips: [...track.clips, clip]
364
+ });
365
+ const newTracks = [...state.timeline.tracks];
366
+ newTracks[trackIndex] = newTrack;
367
+ return {
368
+ ...state,
369
+ timeline: {
370
+ ...state.timeline,
371
+ tracks: newTracks
372
+ }
373
+ };
374
+ }
375
+ function removeClip(state, clipId) {
376
+ const newTracks = state.timeline.tracks.map((track) => ({
377
+ ...track,
378
+ clips: track.clips.filter((c) => c.id !== clipId)
379
+ }));
380
+ return {
381
+ ...state,
382
+ timeline: {
383
+ ...state.timeline,
384
+ tracks: newTracks
385
+ }
386
+ };
387
+ }
388
+ function moveClip(state, clipId, newStart) {
389
+ const clip = findClipById(state, clipId);
390
+ if (!clip) {
391
+ return state;
392
+ }
393
+ const duration = clip.timelineEnd - clip.timelineStart;
394
+ const newEnd = newStart + duration;
395
+ return updateClip(state, clipId, {
396
+ timelineStart: newStart,
397
+ timelineEnd: newEnd
398
+ });
399
+ }
400
+ function resizeClip(state, clipId, newStart, newEnd) {
401
+ const clip = findClipById(state, clipId);
402
+ if (!clip) {
403
+ return state;
404
+ }
405
+ const startDelta = newStart - clip.timelineStart;
406
+ const endDelta = newEnd - clip.timelineEnd;
407
+ let newMediaIn = clip.mediaIn;
408
+ let newMediaOut = clip.mediaOut;
409
+ if (startDelta !== 0) {
410
+ newMediaIn = clip.mediaIn + startDelta;
411
+ }
412
+ if (endDelta !== 0) {
413
+ newMediaOut = clip.mediaOut + endDelta;
414
+ }
415
+ return updateClip(state, clipId, {
416
+ timelineStart: newStart,
417
+ timelineEnd: newEnd,
418
+ mediaIn: newMediaIn,
419
+ mediaOut: newMediaOut
420
+ });
421
+ }
422
+ function trimClip(state, clipId, newMediaIn, newMediaOut) {
423
+ return updateClip(state, clipId, {
424
+ mediaIn: newMediaIn,
425
+ mediaOut: newMediaOut
426
+ });
427
+ }
428
+ function updateClip(state, clipId, updates) {
429
+ const newTracks = state.timeline.tracks.map((track) => {
430
+ const clipIndex = track.clips.findIndex((c) => c.id === clipId);
431
+ if (clipIndex === -1) {
432
+ return track;
433
+ }
434
+ const newClips = [...track.clips];
435
+ const existingClip = newClips[clipIndex];
436
+ if (!existingClip) {
437
+ return track;
438
+ }
439
+ newClips[clipIndex] = {
440
+ ...existingClip,
441
+ ...updates
442
+ };
443
+ return sortTrackClips({
444
+ ...track,
445
+ clips: newClips
446
+ });
447
+ });
448
+ return {
449
+ ...state,
450
+ timeline: {
451
+ ...state.timeline,
452
+ tracks: newTracks
453
+ }
454
+ };
455
+ }
456
+ function moveClipToTrack(state, clipId, targetTrackId) {
457
+ const clip = findClipById(state, clipId);
458
+ if (!clip) {
459
+ return state;
460
+ }
461
+ const targetTrack = findTrackById(state, targetTrackId);
462
+ if (!targetTrack) {
463
+ return state;
464
+ }
465
+ const validationResult = validateTrackTypeMatch(state, clip, targetTrack);
466
+ if (!validationResult.valid) {
467
+ return state;
468
+ }
469
+ if (clip.trackId === targetTrackId) {
470
+ return state;
471
+ }
472
+ let newState = removeClip(state, clipId);
473
+ const updatedClip = { ...clip, trackId: targetTrackId };
474
+ newState = addClip(newState, targetTrackId, updatedClip);
475
+ return newState;
476
+ }
477
+
478
+ // src/operations/track-operations.ts
479
+ function addTrack(state, track) {
480
+ return {
481
+ ...state,
482
+ timeline: {
483
+ ...state.timeline,
484
+ tracks: [...state.timeline.tracks, track]
485
+ }
486
+ };
487
+ }
488
+ function removeTrack(state, trackId) {
489
+ return {
490
+ ...state,
491
+ timeline: {
492
+ ...state.timeline,
493
+ tracks: state.timeline.tracks.filter((t) => t.id !== trackId)
494
+ }
495
+ };
496
+ }
497
+ function moveTrack(state, trackId, newIndex) {
498
+ const currentIndex = findTrackIndex(state, trackId);
499
+ if (currentIndex === -1) {
500
+ return state;
501
+ }
502
+ const newTracks = [...state.timeline.tracks];
503
+ const [track] = newTracks.splice(currentIndex, 1);
504
+ if (!track) {
505
+ return state;
506
+ }
507
+ newTracks.splice(newIndex, 0, track);
508
+ return {
509
+ ...state,
510
+ timeline: {
511
+ ...state.timeline,
512
+ tracks: newTracks
513
+ }
514
+ };
515
+ }
516
+ function updateTrack(state, trackId, updates) {
517
+ const trackIndex = findTrackIndex(state, trackId);
518
+ if (trackIndex === -1) {
519
+ return state;
520
+ }
521
+ const newTracks = [...state.timeline.tracks];
522
+ const existingTrack = newTracks[trackIndex];
523
+ if (!existingTrack) {
524
+ return state;
525
+ }
526
+ newTracks[trackIndex] = {
527
+ ...existingTrack,
528
+ ...updates
529
+ };
530
+ return {
531
+ ...state,
532
+ timeline: {
533
+ ...state.timeline,
534
+ tracks: newTracks
535
+ }
536
+ };
537
+ }
538
+ function toggleTrackMute(state, trackId) {
539
+ const trackIndex = findTrackIndex(state, trackId);
540
+ if (trackIndex === -1) {
541
+ return state;
542
+ }
543
+ const track = state.timeline.tracks[trackIndex];
544
+ if (!track) {
545
+ return state;
546
+ }
547
+ return updateTrack(state, trackId, { muted: !track.muted });
548
+ }
549
+ function toggleTrackLock(state, trackId) {
550
+ const trackIndex = findTrackIndex(state, trackId);
551
+ if (trackIndex === -1) {
552
+ return state;
553
+ }
554
+ const track = state.timeline.tracks[trackIndex];
555
+ if (!track) {
556
+ return state;
557
+ }
558
+ return updateTrack(state, trackId, { locked: !track.locked });
559
+ }
560
+
561
+ // src/operations/timeline-operations.ts
562
+ function setTimelineDuration(state, duration) {
563
+ return {
564
+ ...state,
565
+ timeline: {
566
+ ...state.timeline,
567
+ duration
568
+ }
569
+ };
570
+ }
571
+ function setTimelineName(state, name) {
572
+ return {
573
+ ...state,
574
+ timeline: {
575
+ ...state.timeline,
576
+ name
577
+ }
578
+ };
579
+ }
580
+ function updateTimelineMetadata(state, metadata) {
581
+ return {
582
+ ...state,
583
+ timeline: {
584
+ ...state.timeline,
585
+ metadata: {
586
+ ...state.timeline.metadata,
587
+ ...metadata
588
+ }
589
+ }
590
+ };
591
+ }
592
+
593
+ // src/engine/history.ts
594
+ function createHistory(initialState, limit = 50) {
595
+ return {
596
+ past: [],
597
+ present: initialState,
598
+ future: [],
599
+ limit
600
+ };
601
+ }
602
+ function pushHistory(history, newState) {
603
+ const newPast = [...history.past, history.present];
604
+ if (newPast.length > history.limit) {
605
+ newPast.shift();
606
+ }
607
+ return {
608
+ ...history,
609
+ past: newPast,
610
+ present: newState,
611
+ future: []
612
+ // Clear future on new action
613
+ };
614
+ }
615
+ function undo(history) {
616
+ if (history.past.length === 0) {
617
+ return history;
618
+ }
619
+ const newPast = [...history.past];
620
+ const previous = newPast.pop();
621
+ return {
622
+ ...history,
623
+ past: newPast,
624
+ present: previous,
625
+ future: [history.present, ...history.future]
626
+ };
627
+ }
628
+ function redo(history) {
629
+ if (history.future.length === 0) {
630
+ return history;
631
+ }
632
+ const newFuture = [...history.future];
633
+ const next = newFuture.shift();
634
+ return {
635
+ ...history,
636
+ past: [...history.past, history.present],
637
+ present: next,
638
+ future: newFuture
639
+ };
640
+ }
641
+ function canUndo(history) {
642
+ return history.past.length > 0;
643
+ }
644
+ function canRedo(history) {
645
+ return history.future.length > 0;
646
+ }
647
+ function getCurrentState(history) {
648
+ return history.present;
649
+ }
650
+
651
+ // src/engine/dispatcher.ts
652
+ function dispatch(history, operation) {
653
+ const currentState = getCurrentState(history);
654
+ let newState;
655
+ try {
656
+ newState = operation(currentState);
657
+ } catch (error) {
658
+ const errorMessage = error instanceof Error ? error.message : String(error);
659
+ return {
660
+ success: false,
661
+ history,
662
+ // Return unchanged history
663
+ errors: [{
664
+ code: "OPERATION_ERROR",
665
+ message: errorMessage
666
+ }]
667
+ };
668
+ }
669
+ const validationResult = validateTimeline(newState);
670
+ if (!validationResult.valid) {
671
+ return {
672
+ success: false,
673
+ history,
674
+ // Return unchanged history
675
+ errors: validationResult.errors
676
+ };
677
+ }
678
+ const newHistory = pushHistory(history, newState);
679
+ return {
680
+ success: true,
681
+ history: newHistory
682
+ };
683
+ }
684
+
685
+ // src/engine/timeline-engine.ts
686
+ var TimelineEngine = class {
687
+ history;
688
+ listeners = /* @__PURE__ */ new Set();
689
+ /**
690
+ * Create a new timeline engine
691
+ *
692
+ * @param initialState - Initial timeline state
693
+ * @param historyLimit - Maximum number of undo steps (default: 50)
694
+ */
695
+ constructor(initialState, historyLimit = 50) {
696
+ this.history = createHistory(initialState, historyLimit);
697
+ }
698
+ // ===== SUBSCRIPTION =====
699
+ /**
700
+ * Subscribe to state changes
701
+ *
702
+ * The listener will be called whenever the timeline state changes,
703
+ * with the new state passed as an argument.
704
+ * This is used by framework adapters (e.g., React) to trigger re-renders.
705
+ *
706
+ * @param listener - Function to call on state changes, receives new state
707
+ * @returns Unsubscribe function
708
+ *
709
+ * @example
710
+ * ```typescript
711
+ * const unsubscribe = engine.subscribe((state) => {
712
+ * console.log('State changed:', state);
713
+ * });
714
+ *
715
+ * // Later...
716
+ * unsubscribe();
717
+ * ```
718
+ */
719
+ subscribe(listener) {
720
+ this.listeners.add(listener);
721
+ return () => {
722
+ this.listeners.delete(listener);
723
+ };
724
+ }
725
+ /**
726
+ * Notify all subscribers of a state change
727
+ *
728
+ * This is called internally after any operation that modifies state.
729
+ * Framework adapters use this to trigger re-renders.
730
+ */
731
+ notify() {
732
+ const state = this.getState();
733
+ this.listeners.forEach((listener) => listener(state));
734
+ }
735
+ // ===== STATE ACCESS =====
736
+ /**
737
+ * Get the current timeline state
738
+ *
739
+ * @returns Current timeline state
740
+ */
741
+ getState() {
742
+ return getCurrentState(this.history);
743
+ }
744
+ // ===== ASSET OPERATIONS =====
745
+ /**
746
+ * Register an asset
747
+ *
748
+ * @param asset - Asset to register
749
+ * @returns Dispatch result
750
+ */
751
+ registerAsset(asset) {
752
+ const result = dispatch(
753
+ this.history,
754
+ (state) => registerAsset(state, asset)
755
+ );
756
+ if (result.success) {
757
+ this.history = result.history;
758
+ this.notify();
759
+ }
760
+ return result;
761
+ }
762
+ /**
763
+ * Get an asset by ID
764
+ *
765
+ * @param assetId - Asset ID
766
+ * @returns The asset, or undefined if not found
767
+ */
768
+ getAsset(assetId) {
769
+ return getAsset(this.getState(), assetId);
770
+ }
771
+ // ===== CLIP OPERATIONS =====
772
+ /**
773
+ * Add a clip to a track
774
+ *
775
+ * @param trackId - ID of the track to add to
776
+ * @param clip - Clip to add
777
+ * @returns Dispatch result
778
+ */
779
+ addClip(trackId, clip) {
780
+ const result = dispatch(
781
+ this.history,
782
+ (state) => addClip(state, trackId, clip)
783
+ );
784
+ if (result.success) {
785
+ this.history = result.history;
786
+ this.notify();
787
+ }
788
+ return result;
789
+ }
790
+ /**
791
+ * Remove a clip
792
+ *
793
+ * @param clipId - ID of the clip to remove
794
+ * @returns Dispatch result
795
+ */
796
+ removeClip(clipId) {
797
+ const result = dispatch(
798
+ this.history,
799
+ (state) => removeClip(state, clipId)
800
+ );
801
+ if (result.success) {
802
+ this.history = result.history;
803
+ this.notify();
804
+ }
805
+ return result;
806
+ }
807
+ /**
808
+ * Move a clip to a new timeline position
809
+ *
810
+ * @param clipId - ID of the clip to move
811
+ * @param newStart - New timeline start frame
812
+ * @returns Dispatch result
813
+ */
814
+ moveClip(clipId, newStart) {
815
+ const result = dispatch(
816
+ this.history,
817
+ (state) => moveClip(state, clipId, newStart)
818
+ );
819
+ if (result.success) {
820
+ this.history = result.history;
821
+ this.notify();
822
+ }
823
+ return result;
824
+ }
825
+ /**
826
+ * Resize a clip
827
+ *
828
+ * @param clipId - ID of the clip to resize
829
+ * @param newStart - New timeline start frame
830
+ * @param newEnd - New timeline end frame
831
+ * @returns Dispatch result
832
+ */
833
+ resizeClip(clipId, newStart, newEnd) {
834
+ const result = dispatch(
835
+ this.history,
836
+ (state) => resizeClip(state, clipId, newStart, newEnd)
837
+ );
838
+ if (result.success) {
839
+ this.history = result.history;
840
+ this.notify();
841
+ }
842
+ return result;
843
+ }
844
+ /**
845
+ * Trim a clip (change media bounds)
846
+ *
847
+ * @param clipId - ID of the clip to trim
848
+ * @param newMediaIn - New media in frame
849
+ * @param newMediaOut - New media out frame
850
+ * @returns Dispatch result
851
+ */
852
+ trimClip(clipId, newMediaIn, newMediaOut) {
853
+ const result = dispatch(
854
+ this.history,
855
+ (state) => trimClip(state, clipId, newMediaIn, newMediaOut)
856
+ );
857
+ if (result.success) {
858
+ this.history = result.history;
859
+ this.notify();
860
+ }
861
+ return result;
862
+ }
863
+ /**
864
+ * Move a clip to a different track
865
+ *
866
+ * @param clipId - ID of the clip to move
867
+ * @param targetTrackId - ID of the target track
868
+ * @returns Dispatch result
869
+ */
870
+ moveClipToTrack(clipId, targetTrackId) {
871
+ const result = dispatch(
872
+ this.history,
873
+ (state) => moveClipToTrack(state, clipId, targetTrackId)
874
+ );
875
+ if (result.success) {
876
+ this.history = result.history;
877
+ this.notify();
878
+ }
879
+ return result;
880
+ }
881
+ // ===== TRACK OPERATIONS =====
882
+ /**
883
+ * Add a track
884
+ *
885
+ * @param track - Track to add
886
+ * @returns Dispatch result
887
+ */
888
+ addTrack(track) {
889
+ const result = dispatch(
890
+ this.history,
891
+ (state) => addTrack(state, track)
892
+ );
893
+ if (result.success) {
894
+ this.history = result.history;
895
+ this.notify();
896
+ }
897
+ return result;
898
+ }
899
+ /**
900
+ * Remove a track
901
+ *
902
+ * @param trackId - ID of the track to remove
903
+ * @returns Dispatch result
904
+ */
905
+ removeTrack(trackId) {
906
+ const result = dispatch(
907
+ this.history,
908
+ (state) => removeTrack(state, trackId)
909
+ );
910
+ if (result.success) {
911
+ this.history = result.history;
912
+ this.notify();
913
+ }
914
+ return result;
915
+ }
916
+ /**
917
+ * Move a track to a new position
918
+ *
919
+ * @param trackId - ID of the track to move
920
+ * @param newIndex - New index position
921
+ * @returns Dispatch result
922
+ */
923
+ moveTrack(trackId, newIndex) {
924
+ const result = dispatch(
925
+ this.history,
926
+ (state) => moveTrack(state, trackId, newIndex)
927
+ );
928
+ if (result.success) {
929
+ this.history = result.history;
930
+ this.notify();
931
+ }
932
+ return result;
933
+ }
934
+ /**
935
+ * Toggle track mute
936
+ *
937
+ * @param trackId - ID of the track
938
+ * @returns Dispatch result
939
+ */
940
+ toggleTrackMute(trackId) {
941
+ const result = dispatch(
942
+ this.history,
943
+ (state) => toggleTrackMute(state, trackId)
944
+ );
945
+ if (result.success) {
946
+ this.history = result.history;
947
+ this.notify();
948
+ }
949
+ return result;
950
+ }
951
+ /**
952
+ * Toggle track lock
953
+ *
954
+ * @param trackId - ID of the track
955
+ * @returns Dispatch result
956
+ */
957
+ toggleTrackLock(trackId) {
958
+ const result = dispatch(
959
+ this.history,
960
+ (state) => toggleTrackLock(state, trackId)
961
+ );
962
+ if (result.success) {
963
+ this.history = result.history;
964
+ this.notify();
965
+ }
966
+ return result;
967
+ }
968
+ // ===== TIMELINE OPERATIONS =====
969
+ /**
970
+ * Set timeline duration
971
+ *
972
+ * @param duration - New duration in frames
973
+ * @returns Dispatch result
974
+ */
975
+ setTimelineDuration(duration) {
976
+ const result = dispatch(
977
+ this.history,
978
+ (state) => setTimelineDuration(state, duration)
979
+ );
980
+ if (result.success) {
981
+ this.history = result.history;
982
+ this.notify();
983
+ }
984
+ return result;
985
+ }
986
+ /**
987
+ * Set timeline name
988
+ *
989
+ * @param name - New timeline name
990
+ * @returns Dispatch result
991
+ */
992
+ setTimelineName(name) {
993
+ const result = dispatch(
994
+ this.history,
995
+ (state) => setTimelineName(state, name)
996
+ );
997
+ if (result.success) {
998
+ this.history = result.history;
999
+ this.notify();
1000
+ }
1001
+ return result;
1002
+ }
1003
+ // ===== HISTORY OPERATIONS =====
1004
+ /**
1005
+ * Undo the last action
1006
+ *
1007
+ * @returns true if undo was performed
1008
+ */
1009
+ undo() {
1010
+ if (!this.canUndo()) {
1011
+ return false;
1012
+ }
1013
+ this.history = undo(this.history);
1014
+ this.notify();
1015
+ return true;
1016
+ }
1017
+ /**
1018
+ * Redo the last undone action
1019
+ *
1020
+ * @returns true if redo was performed
1021
+ */
1022
+ redo() {
1023
+ if (!this.canRedo()) {
1024
+ return false;
1025
+ }
1026
+ this.history = redo(this.history);
1027
+ this.notify();
1028
+ return true;
1029
+ }
1030
+ /**
1031
+ * Check if undo is available
1032
+ *
1033
+ * @returns true if undo is available
1034
+ */
1035
+ canUndo() {
1036
+ return canUndo(this.history);
1037
+ }
1038
+ /**
1039
+ * Check if redo is available
1040
+ *
1041
+ * @returns true if redo is available
1042
+ */
1043
+ canRedo() {
1044
+ return canRedo(this.history);
1045
+ }
1046
+ // ===== QUERY OPERATIONS =====
1047
+ /**
1048
+ * Find a clip by ID
1049
+ *
1050
+ * @param clipId - Clip ID
1051
+ * @returns The clip, or undefined if not found
1052
+ */
1053
+ findClipById(clipId) {
1054
+ return findClipById(this.getState(), clipId);
1055
+ }
1056
+ /**
1057
+ * Find a track by ID
1058
+ *
1059
+ * @param trackId - Track ID
1060
+ * @returns The track, or undefined if not found
1061
+ */
1062
+ findTrackById(trackId) {
1063
+ return findTrackById(this.getState(), trackId);
1064
+ }
1065
+ /**
1066
+ * Get all clips on a track
1067
+ *
1068
+ * @param trackId - Track ID
1069
+ * @returns Array of clips on the track
1070
+ */
1071
+ getClipsOnTrack(trackId) {
1072
+ return getClipsOnTrack(this.getState(), trackId);
1073
+ }
1074
+ /**
1075
+ * Get all clips at a specific frame
1076
+ *
1077
+ * @param frame - Frame to check
1078
+ * @returns Array of clips at that frame
1079
+ */
1080
+ getClipsAtFrame(frame2) {
1081
+ return getClipsAtFrame(this.getState(), frame2);
1082
+ }
1083
+ /**
1084
+ * Get all clips in a frame range
1085
+ *
1086
+ * @param start - Start frame
1087
+ * @param end - End frame
1088
+ * @returns Array of clips in the range
1089
+ */
1090
+ getClipsInRange(start, end) {
1091
+ return getClipsInRange(this.getState(), start, end);
1092
+ }
1093
+ /**
1094
+ * Get all clips in the timeline
1095
+ *
1096
+ * @returns Array of all clips
1097
+ */
1098
+ getAllClips() {
1099
+ return getAllClips(this.getState());
1100
+ }
1101
+ /**
1102
+ * Get all tracks in the timeline
1103
+ *
1104
+ * @returns Array of all tracks
1105
+ */
1106
+ getAllTracks() {
1107
+ return getAllTracks(this.getState());
1108
+ }
1109
+ };
1110
+
1111
+ // src/types/timeline.ts
1112
+ function createTimeline(params) {
1113
+ const timeline = {
1114
+ id: params.id,
1115
+ name: params.name,
1116
+ fps: params.fps,
1117
+ duration: params.duration,
1118
+ tracks: params.tracks ?? []
1119
+ };
1120
+ if (params.metadata !== void 0) {
1121
+ timeline.metadata = params.metadata;
1122
+ }
1123
+ return timeline;
1124
+ }
1125
+
1126
+ // src/types/asset.ts
1127
+ function createAsset(params) {
1128
+ const asset = {
1129
+ id: params.id,
1130
+ type: params.type,
1131
+ duration: params.duration,
1132
+ sourceUrl: params.sourceUrl
1133
+ };
1134
+ if (params.metadata !== void 0) {
1135
+ asset.metadata = params.metadata;
1136
+ }
1137
+ return asset;
1138
+ }
1139
+
1140
+ // src/types/state.ts
1141
+ function createTimelineState(params) {
1142
+ const state = {
1143
+ timeline: params.timeline,
1144
+ assets: params.assets ?? /* @__PURE__ */ new Map(),
1145
+ linkGroups: params.linkGroups ?? /* @__PURE__ */ new Map(),
1146
+ groups: params.groups ?? /* @__PURE__ */ new Map(),
1147
+ markers: params.markers ?? {
1148
+ timeline: [],
1149
+ clips: [],
1150
+ regions: []
1151
+ }
1152
+ };
1153
+ if (params.workArea !== void 0) {
1154
+ state.workArea = params.workArea;
1155
+ }
1156
+ return state;
1157
+ }
1158
+
1159
+ // src/types/frame.ts
1160
+ function frame(value) {
1161
+ const rounded = Math.round(value);
1162
+ if (rounded < 0) {
1163
+ throw new Error(`Frame value must be non-negative, got: ${value}`);
1164
+ }
1165
+ return rounded;
1166
+ }
1167
+ function frameRate(value) {
1168
+ if (value <= 0) {
1169
+ throw new Error(`FrameRate must be positive, got: ${value}`);
1170
+ }
1171
+ return value;
1172
+ }
1173
+ function isValidFrame(value) {
1174
+ return Number.isInteger(value) && value >= 0;
1175
+ }
1176
+ function isValidFrameRate(value) {
1177
+ return value > 0;
1178
+ }
1179
+
1180
+ // src/utils/frame.ts
1181
+ function framesToSeconds(frames, fps) {
1182
+ return frames / fps;
1183
+ }
1184
+ function secondsToFrames(seconds, fps) {
1185
+ return frame(seconds * fps);
1186
+ }
1187
+ function framesToTimecode(frames, fps) {
1188
+ const totalFrames = frames;
1189
+ const framesPart = totalFrames % fps;
1190
+ const totalSeconds = Math.floor(totalFrames / fps);
1191
+ const secondsPart = totalSeconds % 60;
1192
+ const totalMinutes = Math.floor(totalSeconds / 60);
1193
+ const minutesPart = totalMinutes % 60;
1194
+ const hoursPart = Math.floor(totalMinutes / 60);
1195
+ return `${pad(hoursPart)}:${pad(minutesPart)}:${pad(secondsPart)}:${pad(framesPart)}`;
1196
+ }
1197
+ function framesToMinutesSeconds(frames, fps) {
1198
+ const totalSeconds = Math.floor(frames / fps);
1199
+ const minutes = Math.floor(totalSeconds / 60);
1200
+ const seconds = totalSeconds % 60;
1201
+ return `${minutes}:${pad(seconds)}`;
1202
+ }
1203
+ function clampFrame(value, min, max) {
1204
+ return frame(Math.max(min, Math.min(max, value)));
1205
+ }
1206
+ function addFrames(a, b) {
1207
+ return frame(a + b);
1208
+ }
1209
+ function subtractFrames(a, b) {
1210
+ return frame(Math.max(0, a - b));
1211
+ }
1212
+ function frameDuration(start, end) {
1213
+ return frame(end - start);
1214
+ }
1215
+ function pad(num, width = 2) {
1216
+ return num.toString().padStart(width, "0");
1217
+ }
1218
+
1219
+ export {
1220
+ createClip,
1221
+ getClipDuration,
1222
+ getClipMediaDuration,
1223
+ clipContainsFrame,
1224
+ clipsOverlap,
1225
+ validResult,
1226
+ invalidResult,
1227
+ invalidResults,
1228
+ combineResults,
1229
+ registerAsset,
1230
+ getAsset,
1231
+ hasAsset,
1232
+ getAllAssets,
1233
+ unregisterAsset,
1234
+ validateClip,
1235
+ validateTrack,
1236
+ validateTimeline,
1237
+ validateNoOverlap,
1238
+ findClipById,
1239
+ findTrackById,
1240
+ getClipsOnTrack,
1241
+ getClipsAtFrame,
1242
+ getClipsInRange,
1243
+ getAllClips,
1244
+ getAllTracks,
1245
+ findTrackIndex,
1246
+ createTrack,
1247
+ sortTrackClips,
1248
+ addClip,
1249
+ removeClip,
1250
+ moveClip,
1251
+ resizeClip,
1252
+ trimClip,
1253
+ updateClip,
1254
+ moveClipToTrack,
1255
+ addTrack,
1256
+ removeTrack,
1257
+ moveTrack,
1258
+ updateTrack,
1259
+ toggleTrackMute,
1260
+ toggleTrackLock,
1261
+ setTimelineDuration,
1262
+ setTimelineName,
1263
+ updateTimelineMetadata,
1264
+ TimelineEngine,
1265
+ createTimeline,
1266
+ createAsset,
1267
+ createTimelineState,
1268
+ frame,
1269
+ frameRate,
1270
+ isValidFrame,
1271
+ isValidFrameRate,
1272
+ framesToSeconds,
1273
+ secondsToFrames,
1274
+ framesToTimecode,
1275
+ framesToMinutesSeconds,
1276
+ clampFrame,
1277
+ addFrames,
1278
+ subtractFrames,
1279
+ frameDuration
1280
+ };