@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,1574 @@
1
+ /**
2
+ * FRAME-BASED TIME REPRESENTATION
3
+ *
4
+ * Phase 0 compliant. All time values in state are TimelineFrame branded integers.
5
+ * FrameRate is a discriminated literal union — never a raw float.
6
+ *
7
+ * THREE INVIOLABLE RULES:
8
+ * 1. Core has ZERO UI framework imports.
9
+ * 2. Every function that changes state returns a NEW object.
10
+ * 3. Every frame value is a branded TimelineFrame integer — never a raw number.
11
+ */
12
+ /**
13
+ * TimelineFrame — A discrete, non-negative integer point in time measured in frames.
14
+ *
15
+ * Branded so TypeScript prevents raw numbers from sneaking into frame positions.
16
+ * The ONLY way to create one is via toFrame().
17
+ */
18
+ type TimelineFrame = number & {
19
+ readonly __brand: "TimelineFrame";
20
+ };
21
+ /** The canonical factory. Use this everywhere instead of casting. */
22
+ declare const toFrame: (n: number) => TimelineFrame;
23
+ /**
24
+ * Legacy alias kept for backward-compat during transition.
25
+ * Prefer toFrame() for new code.
26
+ */
27
+ declare function frame(value: number): TimelineFrame;
28
+ /**
29
+ * FrameRate — The exact set of supported frame rates.
30
+ *
31
+ * RULE: Never pass 29.97 as a plain number. Use the literal type.
32
+ * This is a discriminated union — TypeScript enforces membership at compile time.
33
+ */
34
+ type FrameRate = 23.976 | 24 | 25 | 29.97 | 30 | 50 | 59.94 | 60;
35
+ /**
36
+ * Named constants for the most common rates.
37
+ * Prefer these over raw literals where possible.
38
+ */
39
+ declare const FrameRates: {
40
+ readonly CINEMA: FrameRate;
41
+ readonly PAL: FrameRate;
42
+ readonly NTSC_DF: FrameRate;
43
+ readonly NTSC: FrameRate;
44
+ readonly PAL_HFR: FrameRate;
45
+ readonly NTSC_HFR: FrameRate;
46
+ readonly HFR: FrameRate;
47
+ };
48
+ /**
49
+ * Legacy factory — kept for backward-compat with existing tests.
50
+ * This now validates that the value is a member of the FrameRate union.
51
+ * @throws if the value is not a recognised frame rate.
52
+ */
53
+ declare function frameRate(value: number): FrameRate;
54
+ /**
55
+ * RationalTime — a frame count at a specific rate. Used only at
56
+ * ingest/export boundaries. Never stored in TimelineState.
57
+ */
58
+ type RationalTime = {
59
+ readonly value: number;
60
+ readonly rate: FrameRate;
61
+ };
62
+ /**
63
+ * Timecode — SMPTE timecode string, display-only. Never use for arithmetic.
64
+ */
65
+ type Timecode = string & {
66
+ readonly __brand: "Timecode";
67
+ };
68
+ declare const toTimecode: (s: string) => Timecode;
69
+ /**
70
+ * TimeRange — a start + duration pair, both in TimelineFrame units.
71
+ */
72
+ type TimeRange = {
73
+ readonly startFrame: TimelineFrame;
74
+ readonly duration: TimelineFrame;
75
+ };
76
+ declare function isValidFrame(value: number): boolean;
77
+ declare function isDropFrame(fps: FrameRate): boolean;
78
+
79
+ /**
80
+ * GENERATOR MODEL — Phase 3
81
+ *
82
+ * Generators are synthetic "assets" (solid, bars, countdown, etc.)
83
+ * registered in AssetRegistry as GeneratorAsset. No filePath.
84
+ */
85
+
86
+ type GeneratorId = string & {
87
+ readonly __brand: 'GeneratorId';
88
+ };
89
+ type GeneratorType = 'solid' | 'bars' | 'countdown' | 'noise' | 'text';
90
+ type Generator = {
91
+ readonly id: GeneratorId;
92
+ readonly type: GeneratorType;
93
+ readonly params: Record<string, unknown>;
94
+ readonly duration: TimelineFrame;
95
+ readonly name: string;
96
+ };
97
+
98
+ /**
99
+ * ASSET MODEL — Phase 0 + Phase 3
100
+ *
101
+ * Asset is FileAsset | GeneratorAsset. Multiple Clips can reference the same Asset.
102
+ * Assets never change their intrinsicDuration after registration.
103
+ */
104
+
105
+ type AssetId = string & {
106
+ readonly __brand: 'AssetId';
107
+ };
108
+ declare const toAssetId: (s: string) => AssetId;
109
+ type AssetStatus = 'online' | 'offline' | 'proxy-only' | 'missing';
110
+ type FileAsset = {
111
+ readonly kind: 'file';
112
+ readonly id: AssetId;
113
+ readonly name: string;
114
+ readonly mediaType: TrackType;
115
+ readonly filePath: string;
116
+ readonly intrinsicDuration: TimelineFrame;
117
+ readonly nativeFps: FrameRate;
118
+ readonly sourceTimecodeOffset: TimelineFrame;
119
+ readonly status: AssetStatus;
120
+ };
121
+ type GeneratorAsset = {
122
+ readonly kind: 'generator';
123
+ readonly id: AssetId;
124
+ readonly name: string;
125
+ readonly mediaType: TrackType;
126
+ readonly intrinsicDuration: TimelineFrame;
127
+ readonly nativeFps: FrameRate;
128
+ readonly sourceTimecodeOffset: TimelineFrame;
129
+ readonly status: AssetStatus;
130
+ readonly generatorDef: Generator;
131
+ };
132
+ type Asset = FileAsset | GeneratorAsset;
133
+ declare function createAsset(params: {
134
+ id: string;
135
+ name: string;
136
+ mediaType: TrackType;
137
+ filePath: string;
138
+ intrinsicDuration: TimelineFrame;
139
+ nativeFps: FrameRate;
140
+ sourceTimecodeOffset: TimelineFrame;
141
+ status?: AssetStatus;
142
+ }): FileAsset;
143
+
144
+ /**
145
+ * CLIP MODEL — Phase 0 compliant
146
+ *
147
+ * A Clip is a time-bound reference to an Asset placed on a Track.
148
+ * All fields are readonly. Never mutate — always return a new object.
149
+ */
150
+
151
+ type ClipId = string & {
152
+ readonly __brand: 'ClipId';
153
+ };
154
+ declare const toClipId: (s: string) => ClipId;
155
+ /**
156
+ * Clip — a time-bound viewport into an Asset on a Track.
157
+ *
158
+ * TIMELINE BOUNDS: timelineStart / timelineEnd — where it sits on the track.
159
+ * MEDIA BOUNDS: mediaIn / mediaOut — which portion of the asset plays.
160
+ *
161
+ * INVARIANTS (Phase 0, speed=1.0):
162
+ * timelineEnd > timelineStart
163
+ * mediaOut > mediaIn
164
+ * (mediaOut - mediaIn) === (timelineEnd - timelineStart)
165
+ * mediaIn >= 0
166
+ * mediaOut <= asset.intrinsicDuration
167
+ * timelineEnd <= timeline.duration
168
+ * speed > 0
169
+ */
170
+ type Clip = {
171
+ readonly id: ClipId;
172
+ readonly assetId: AssetId;
173
+ readonly trackId: TrackId;
174
+ readonly timelineStart: TimelineFrame;
175
+ readonly timelineEnd: TimelineFrame;
176
+ readonly mediaIn: TimelineFrame;
177
+ readonly mediaOut: TimelineFrame;
178
+ readonly speed: number;
179
+ readonly enabled: boolean;
180
+ readonly reversed: boolean;
181
+ readonly name: string | null;
182
+ readonly color: string | null;
183
+ readonly metadata: Record<string, string>;
184
+ };
185
+ declare function createClip(params: {
186
+ id: string;
187
+ assetId: string;
188
+ trackId: string;
189
+ timelineStart: TimelineFrame;
190
+ timelineEnd: TimelineFrame;
191
+ mediaIn: TimelineFrame;
192
+ mediaOut: TimelineFrame;
193
+ speed?: number;
194
+ enabled?: boolean;
195
+ reversed?: boolean;
196
+ name?: string | null;
197
+ color?: string | null;
198
+ metadata?: Record<string, string>;
199
+ }): Clip;
200
+ declare function getClipDuration(clip: Clip): TimelineFrame;
201
+ declare function getClipMediaDuration(clip: Clip): TimelineFrame;
202
+ declare function clipContainsFrame(clip: Clip, f: TimelineFrame): boolean;
203
+ declare function clipsOverlap(a: Clip, b: Clip): boolean;
204
+
205
+ /**
206
+ * CAPTION MODEL — Phase 3
207
+ *
208
+ * Captions live on Track.captions[]. Used for SRT/VTT and burn-in.
209
+ */
210
+
211
+ type CaptionId = string & {
212
+ readonly __brand: 'CaptionId';
213
+ };
214
+ type CaptionStyle = {
215
+ readonly fontFamily: string;
216
+ readonly fontSize: number;
217
+ readonly color: string;
218
+ readonly backgroundColor: string;
219
+ readonly hAlign: 'left' | 'center' | 'right';
220
+ readonly vAlign: 'top' | 'center' | 'bottom';
221
+ };
222
+ type Caption = {
223
+ readonly id: CaptionId;
224
+ readonly text: string;
225
+ readonly startFrame: TimelineFrame;
226
+ readonly endFrame: TimelineFrame;
227
+ readonly language: string;
228
+ readonly style: CaptionStyle;
229
+ readonly burnIn: boolean;
230
+ };
231
+
232
+ /**
233
+ * TRACK MODEL — Phase 0 + Phase 3
234
+ *
235
+ * A Track is a horizontal container for Clips, always sorted by timelineStart.
236
+ * Phase 3: captions[] for subtitle/caption items.
237
+ */
238
+
239
+ type TrackId = string & {
240
+ readonly __brand: 'TrackId';
241
+ };
242
+ declare const toTrackId: (s: string) => TrackId;
243
+ type TrackType = 'video' | 'audio' | 'subtitle' | 'title';
244
+ type Track = {
245
+ readonly id: TrackId;
246
+ readonly name: string;
247
+ readonly type: TrackType;
248
+ readonly locked: boolean;
249
+ readonly muted: boolean;
250
+ readonly solo: boolean;
251
+ readonly height: number;
252
+ /** Always sorted ascending by timelineStart — invariant enforced by checkInvariants. */
253
+ readonly clips: readonly Clip[];
254
+ /** Phase 3: captions on this track (e.g. subtitle/title). */
255
+ readonly captions: readonly Caption[];
256
+ };
257
+ declare function createTrack(params: {
258
+ id: string;
259
+ name: string;
260
+ type: TrackType;
261
+ clips?: readonly Clip[];
262
+ captions?: readonly Caption[];
263
+ locked?: boolean;
264
+ muted?: boolean;
265
+ solo?: boolean;
266
+ height?: number;
267
+ }): Track;
268
+ /** Returns a new track with clips sorted ascending by timelineStart. */
269
+ declare function sortTrackClips(track: Track): Track;
270
+
271
+ /**
272
+ * MARKER TYPES — Phase 3
273
+ *
274
+ * Discriminated union: point (single frame) or range (frameStart..frameEnd).
275
+ * Markers live on Timeline.markers[]. linkedClipId moves with clip on ripple.
276
+ */
277
+
278
+ type MarkerId = string & {
279
+ readonly __brand: 'MarkerId';
280
+ };
281
+ type MarkerScope = 'global' | 'personal' | 'export';
282
+ type Marker = {
283
+ readonly type: 'point';
284
+ readonly id: MarkerId;
285
+ readonly frame: TimelineFrame;
286
+ readonly label: string;
287
+ readonly color: string;
288
+ readonly scope: MarkerScope;
289
+ readonly linkedClipId: ClipId | null;
290
+ readonly clipId?: ClipId;
291
+ } | {
292
+ readonly type: 'range';
293
+ readonly id: MarkerId;
294
+ readonly frameStart: TimelineFrame;
295
+ readonly frameEnd: TimelineFrame;
296
+ readonly label: string;
297
+ readonly color: string;
298
+ readonly scope: MarkerScope;
299
+ readonly linkedClipId: ClipId | null;
300
+ readonly clipId?: ClipId;
301
+ };
302
+ type BeatGrid = {
303
+ readonly bpm: number;
304
+ readonly timeSignature: readonly [number, number];
305
+ readonly offset: TimelineFrame;
306
+ };
307
+
308
+ /**
309
+ * TIMELINE MODEL — Phase 0 + Phase 3
310
+ */
311
+
312
+ type SequenceSettings = {
313
+ readonly pixelAspectRatio: number;
314
+ readonly fieldOrder: 'progressive' | 'upper' | 'lower';
315
+ readonly colorSpace: string;
316
+ readonly audioSampleRate: number;
317
+ readonly audioChannelCount: number;
318
+ };
319
+ type Timeline = {
320
+ readonly id: string;
321
+ readonly name: string;
322
+ readonly fps: FrameRate;
323
+ readonly duration: TimelineFrame;
324
+ readonly startTimecode: Timecode;
325
+ readonly tracks: readonly Track[];
326
+ readonly sequenceSettings: SequenceSettings;
327
+ /**
328
+ * Increments by 1 on every successfully committed Transaction.
329
+ * Use this to detect stale references without deep equality checks.
330
+ */
331
+ readonly version: number;
332
+ readonly markers: readonly Marker[];
333
+ readonly beatGrid: BeatGrid | null;
334
+ readonly inPoint: TimelineFrame | null;
335
+ readonly outPoint: TimelineFrame | null;
336
+ };
337
+ declare function createTimeline(params: {
338
+ id: string;
339
+ name: string;
340
+ fps: FrameRate;
341
+ duration: TimelineFrame;
342
+ startTimecode?: Timecode;
343
+ tracks?: readonly Track[];
344
+ sequenceSettings?: Partial<SequenceSettings>;
345
+ markers?: readonly Marker[];
346
+ beatGrid?: BeatGrid | null;
347
+ inPoint?: TimelineFrame | null;
348
+ outPoint?: TimelineFrame | null;
349
+ }): Timeline;
350
+
351
+ /**
352
+ * TIMELINE STATE — Phase 0 compliant
353
+ *
354
+ * TimelineState is the single source of truth for the engine.
355
+ * Phase 0 only: timeline + assetRegistry. No Phase 2 fields.
356
+ *
357
+ * RULE: Every function that changes state returns a NEW TimelineState.
358
+ * Never mutate the existing state.
359
+ */
360
+
361
+ type AssetRegistry = ReadonlyMap<AssetId, Asset>;
362
+ /**
363
+ * Increment this whenever TimelineState gains a new required field or
364
+ * a field's semantics change in a breaking way.
365
+ *
366
+ * The schemaVersion invariant check rejects loading a future schema
367
+ * into an older engine (prevents silent data corruption on downgrade).
368
+ */
369
+ declare const CURRENT_SCHEMA_VERSION: 1;
370
+ type TimelineState = {
371
+ readonly schemaVersion: number;
372
+ readonly timeline: Timeline;
373
+ readonly assetRegistry: AssetRegistry;
374
+ };
375
+ declare function createTimelineState(params: {
376
+ timeline: Timeline;
377
+ assetRegistry?: AssetRegistry;
378
+ /** @deprecated use assetRegistry. Kept for test backward-compat only. */
379
+ assets?: Map<string, Asset>;
380
+ }): TimelineState;
381
+
382
+ /**
383
+ * FRAME UTILITIES
384
+ *
385
+ * Pure functions for working with frame-based time values.
386
+ *
387
+ * These utilities handle:
388
+ * - Converting between frames and seconds
389
+ * - Formatting frames as timecode (HH:MM:SS:FF)
390
+ * - Frame arithmetic (clamping, rounding)
391
+ *
392
+ * CRITICAL RULES:
393
+ * - All conversions must quantize to whole frames
394
+ * - No floating-point frame values allowed
395
+ * - Always round/floor/ceil explicitly
396
+ *
397
+ * USAGE:
398
+ * ```typescript
399
+ * const fps = frameRate(30);
400
+ * const frames = secondsToFrames(5.5, fps); // 165 frames
401
+ * const seconds = framesToSeconds(frames, fps); // 5.5 seconds
402
+ * const timecode = framesToTimecode(frames, fps); // "00:00:05:15"
403
+ * ```
404
+ */
405
+
406
+ /**
407
+ * Convert frames to seconds
408
+ *
409
+ * @param frames - Frame number
410
+ * @param fps - Frames per second
411
+ * @returns Time in seconds (may be fractional)
412
+ */
413
+ declare function framesToSeconds(frames: TimelineFrame, fps: FrameRate): number;
414
+ /**
415
+ * Convert seconds to frames
416
+ *
417
+ * IMPORTANT: This rounds to the nearest frame.
418
+ * If you need different rounding behavior, use Math.floor or Math.ceil explicitly.
419
+ *
420
+ * @param seconds - Time in seconds
421
+ * @param fps - Frames per second
422
+ * @returns Frame number (rounded to nearest frame)
423
+ */
424
+ declare function secondsToFrames(seconds: number, fps: FrameRate): TimelineFrame;
425
+ /**
426
+ * Convert frames to timecode format (HH:MM:SS:FF)
427
+ *
428
+ * Example: 3825 frames at 30fps = "00:02:07:15"
429
+ *
430
+ * @param frames - Frame number
431
+ * @param fps - Frames per second
432
+ * @returns Timecode string
433
+ */
434
+ declare function framesToTimecode(frames: TimelineFrame, fps: FrameRate): string;
435
+ /**
436
+ * Convert frames to simple MM:SS format
437
+ *
438
+ * Example: 3825 frames at 30fps = "02:07"
439
+ *
440
+ * @param frames - Frame number
441
+ * @param fps - Frames per second
442
+ * @returns Time string in MM:SS format
443
+ */
444
+ declare function framesToMinutesSeconds(frames: TimelineFrame, fps: FrameRate): string;
445
+ /**
446
+ * Clamp a frame value between min and max
447
+ *
448
+ * @param value - Frame to clamp
449
+ * @param min - Minimum frame (inclusive)
450
+ * @param max - Maximum frame (inclusive)
451
+ * @returns Clamped frame value
452
+ */
453
+ declare function clampFrame(value: TimelineFrame, min: TimelineFrame, max: TimelineFrame): TimelineFrame;
454
+ /**
455
+ * Add two frame values
456
+ *
457
+ * @param a - First frame
458
+ * @param b - Second frame
459
+ * @returns Sum of frames
460
+ */
461
+ declare function addFrames(a: TimelineFrame, b: TimelineFrame): TimelineFrame;
462
+ /**
463
+ * Subtract two frame values
464
+ *
465
+ * @param a - First frame
466
+ * @param b - Second frame (subtracted from a)
467
+ * @returns Difference of frames (clamped to 0 if negative)
468
+ */
469
+ declare function subtractFrames(a: TimelineFrame, b: TimelineFrame): TimelineFrame;
470
+ /**
471
+ * Calculate duration between two frames
472
+ *
473
+ * @param start - Start frame
474
+ * @param end - End frame
475
+ * @returns Duration in frames (end - start)
476
+ */
477
+ declare function frameDuration(start: TimelineFrame, end: TimelineFrame): TimelineFrame;
478
+
479
+ /**
480
+ * TIMELINE ENGINE
481
+ *
482
+ * The main public API for the timeline editing kernel.
483
+ *
484
+ * WHAT IS THE TIMELINE ENGINE?
485
+ * - A thin wrapper around the history and dispatch systems
486
+ * - Provides a convenient, object-oriented API
487
+ * - Manages internal state
488
+ * - Coordinates operations, validation, and history
489
+ *
490
+ * WHY A CLASS?
491
+ * - Encapsulates state management
492
+ * - Provides a clean API for users
493
+ * - Hides complexity of history and dispatch
494
+ * - Familiar OOP interface for most developers
495
+ *
496
+ * USAGE:
497
+ * ```typescript
498
+ * const engine = new TimelineEngine(initialState);
499
+ *
500
+ * // Add a clip
501
+ * const result = engine.addClip(trackId, clip);
502
+ * if (!result.success) {
503
+ * console.error('Failed to add clip:', result.errors);
504
+ * }
505
+ *
506
+ * // Undo/redo
507
+ * engine.undo();
508
+ * engine.redo();
509
+ *
510
+ * // Query state
511
+ * const clip = engine.findClipById('clip_1');
512
+ * const state = engine.getState();
513
+ * ```
514
+ *
515
+ * DESIGN PHILOSOPHY:
516
+ * - Business logic lives in pure modules (operations, validation, etc.)
517
+ * - Engine is just a thin orchestration layer
518
+ * - Easy to test (can test pure functions independently)
519
+ */
520
+
521
+ /**
522
+ * TimelineEngine - The main timeline editing engine
523
+ *
524
+ * Provides a high-level API for timeline editing with built-in
525
+ * undo/redo, validation, and state management.
526
+ */
527
+ declare class TimelineEngine {
528
+ private history;
529
+ private listeners;
530
+ /**
531
+ * Create a new timeline engine
532
+ *
533
+ * @param initialState - Initial timeline state
534
+ * @param historyLimit - Maximum number of undo steps (default: 50)
535
+ */
536
+ constructor(initialState: TimelineState, historyLimit?: number);
537
+ /**
538
+ * Subscribe to state changes
539
+ *
540
+ * The listener will be called whenever the timeline state changes,
541
+ * with the new state passed as an argument.
542
+ * This is used by framework adapters (e.g., React) to trigger re-renders.
543
+ *
544
+ * @param listener - Function to call on state changes, receives new state
545
+ * @returns Unsubscribe function
546
+ *
547
+ * @example
548
+ * ```typescript
549
+ * const unsubscribe = engine.subscribe((state) => {
550
+ * console.log('State changed:', state);
551
+ * });
552
+ *
553
+ * // Later...
554
+ * unsubscribe();
555
+ * ```
556
+ */
557
+ subscribe(listener: (state: TimelineState) => void): () => void;
558
+ /**
559
+ * Notify all subscribers of a state change
560
+ *
561
+ * This is called internally after any operation that modifies state.
562
+ * Framework adapters use this to trigger re-renders.
563
+ */
564
+ private notify;
565
+ /**
566
+ * Get the current timeline state
567
+ *
568
+ * @returns Current timeline state
569
+ */
570
+ getState(): TimelineState;
571
+ /**
572
+ * Register an asset
573
+ *
574
+ * @param asset - Asset to register
575
+ * @returns Dispatch result
576
+ */
577
+ registerAsset(asset: Asset): {
578
+ accepted: boolean;
579
+ errors?: {
580
+ code: string;
581
+ message: string;
582
+ }[];
583
+ };
584
+ /**
585
+ * Get an asset by ID
586
+ *
587
+ * @param assetId - Asset ID
588
+ * @returns The asset, or undefined if not found
589
+ */
590
+ getAsset(assetId: string): Asset | undefined;
591
+ /**
592
+ * Add a clip to a track
593
+ *
594
+ * @param trackId - ID of the track to add to
595
+ * @param clip - Clip to add
596
+ * @returns Dispatch result
597
+ */
598
+ addClip(trackId: string, clip: Clip): {
599
+ accepted: boolean;
600
+ errors?: {
601
+ code: string;
602
+ message: string;
603
+ }[];
604
+ };
605
+ /**
606
+ * Remove a clip
607
+ *
608
+ * @param clipId - ID of the clip to remove
609
+ * @returns Dispatch result
610
+ */
611
+ removeClip(clipId: string): {
612
+ accepted: boolean;
613
+ errors?: {
614
+ code: string;
615
+ message: string;
616
+ }[];
617
+ };
618
+ /**
619
+ * Move a clip to a new timeline position
620
+ *
621
+ * @param clipId - ID of the clip to move
622
+ * @param newStart - New timeline start frame
623
+ * @returns Dispatch result
624
+ */
625
+ moveClip(clipId: string, newStart: TimelineFrame): {
626
+ accepted: boolean;
627
+ errors?: {
628
+ code: string;
629
+ message: string;
630
+ }[];
631
+ };
632
+ /**
633
+ * Resize a clip
634
+ *
635
+ * @param clipId - ID of the clip to resize
636
+ * @param newStart - New timeline start frame
637
+ * @param newEnd - New timeline end frame
638
+ * @returns Dispatch result
639
+ */
640
+ resizeClip(clipId: string, newStart: TimelineFrame, newEnd: TimelineFrame): {
641
+ accepted: boolean;
642
+ errors?: {
643
+ code: string;
644
+ message: string;
645
+ }[];
646
+ };
647
+ /**
648
+ * Trim a clip (change media bounds)
649
+ *
650
+ * @param clipId - ID of the clip to trim
651
+ * @param newMediaIn - New media in frame
652
+ * @param newMediaOut - New media out frame
653
+ * @returns Dispatch result
654
+ */
655
+ trimClip(clipId: string, newMediaIn: TimelineFrame, newMediaOut: TimelineFrame): {
656
+ accepted: boolean;
657
+ errors?: {
658
+ code: string;
659
+ message: string;
660
+ }[];
661
+ };
662
+ /**
663
+ * Move a clip to a different track
664
+ *
665
+ * @param clipId - ID of the clip to move
666
+ * @param targetTrackId - ID of the target track
667
+ * @returns Dispatch result
668
+ */
669
+ moveClipToTrack(clipId: string, targetTrackId: string): {
670
+ accepted: boolean;
671
+ errors?: {
672
+ code: string;
673
+ message: string;
674
+ }[];
675
+ };
676
+ /**
677
+ * Add a track
678
+ *
679
+ * @param track - Track to add
680
+ * @returns Dispatch result
681
+ */
682
+ addTrack(track: Track): {
683
+ accepted: boolean;
684
+ errors?: {
685
+ code: string;
686
+ message: string;
687
+ }[];
688
+ };
689
+ /**
690
+ * Remove a track
691
+ *
692
+ * @param trackId - ID of the track to remove
693
+ * @returns Dispatch result
694
+ */
695
+ removeTrack(trackId: string): {
696
+ accepted: boolean;
697
+ errors?: {
698
+ code: string;
699
+ message: string;
700
+ }[];
701
+ };
702
+ /**
703
+ * Move a track to a new position
704
+ *
705
+ * @param trackId - ID of the track to move
706
+ * @param newIndex - New index position
707
+ * @returns Dispatch result
708
+ */
709
+ moveTrack(trackId: string, newIndex: number): {
710
+ accepted: boolean;
711
+ errors?: {
712
+ code: string;
713
+ message: string;
714
+ }[];
715
+ };
716
+ /**
717
+ * Toggle track mute
718
+ *
719
+ * @param trackId - ID of the track
720
+ * @returns Dispatch result
721
+ */
722
+ toggleTrackMute(trackId: string): {
723
+ accepted: boolean;
724
+ errors?: {
725
+ code: string;
726
+ message: string;
727
+ }[];
728
+ };
729
+ /**
730
+ * Toggle track lock
731
+ *
732
+ * @param trackId - ID of the track
733
+ * @returns Dispatch result
734
+ */
735
+ toggleTrackLock(trackId: string): {
736
+ accepted: boolean;
737
+ errors?: {
738
+ code: string;
739
+ message: string;
740
+ }[];
741
+ };
742
+ /**
743
+ * Toggle track solo
744
+ *
745
+ * @param trackId - ID of the track
746
+ * @returns Dispatch result
747
+ */
748
+ toggleTrackSolo(trackId: string): {
749
+ accepted: boolean;
750
+ errors?: {
751
+ code: string;
752
+ message: string;
753
+ }[];
754
+ };
755
+ /**
756
+ * Set track height
757
+ *
758
+ * @param trackId - ID of the track
759
+ * @param height - New height in pixels
760
+ * @returns Dispatch result
761
+ */
762
+ setTrackHeight(trackId: string, height: number): {
763
+ accepted: boolean;
764
+ errors?: {
765
+ code: string;
766
+ message: string;
767
+ }[];
768
+ };
769
+ /**
770
+ * Set timeline duration
771
+ *
772
+ * @param duration - New duration in frames
773
+ * @returns Dispatch result
774
+ */
775
+ setTimelineDuration(duration: TimelineFrame): {
776
+ accepted: boolean;
777
+ errors?: {
778
+ code: string;
779
+ message: string;
780
+ }[];
781
+ };
782
+ /**
783
+ * Set timeline name
784
+ *
785
+ * @param name - New timeline name
786
+ * @returns Dispatch result
787
+ */
788
+ setTimelineName(name: string): {
789
+ accepted: boolean;
790
+ errors?: {
791
+ code: string;
792
+ message: string;
793
+ }[];
794
+ };
795
+ /**
796
+ * Undo the last action
797
+ *
798
+ * @returns true if undo was performed
799
+ */
800
+ undo(): boolean;
801
+ /**
802
+ * Redo the last undone action
803
+ *
804
+ * @returns true if redo was performed
805
+ */
806
+ redo(): boolean;
807
+ /**
808
+ * Check if undo is available
809
+ *
810
+ * @returns true if undo is available
811
+ */
812
+ canUndo(): boolean;
813
+ /**
814
+ * Check if redo is available
815
+ *
816
+ * @returns true if redo is available
817
+ */
818
+ canRedo(): boolean;
819
+ /**
820
+ * Find a clip by ID
821
+ *
822
+ * @param clipId - Clip ID
823
+ * @returns The clip, or undefined if not found
824
+ */
825
+ findClipById(clipId: string): Clip | undefined;
826
+ /**
827
+ * Find a track by ID
828
+ *
829
+ * @param trackId - Track ID
830
+ * @returns The track, or undefined if not found
831
+ */
832
+ findTrackById(trackId: string): Track | undefined;
833
+ /**
834
+ * Get all clips on a track
835
+ *
836
+ * @param trackId - Track ID
837
+ * @returns Array of clips on the track
838
+ */
839
+ getClipsOnTrack(trackId: string): Clip[];
840
+ /**
841
+ * Get all clips at a specific frame
842
+ *
843
+ * @param frame - Frame to check
844
+ * @returns Array of clips at that frame
845
+ */
846
+ getClipsAtFrame(f: TimelineFrame): Clip[];
847
+ /**
848
+ * Get all clips in a frame range
849
+ *
850
+ * @param start - Start frame
851
+ * @param end - End frame
852
+ * @returns Array of clips in the range
853
+ */
854
+ getClipsInRange(start: TimelineFrame, end: TimelineFrame): Clip[];
855
+ /**
856
+ * Get all clips in the timeline
857
+ *
858
+ * @returns Array of all clips
859
+ */
860
+ getAllClips(): Clip[];
861
+ /**
862
+ * Get all tracks in the timeline
863
+ *
864
+ * @returns Array of all tracks
865
+ */
866
+ getAllTracks(): readonly Track[];
867
+ /**
868
+ * Ripple delete - delete clip and shift subsequent clips left
869
+ *
870
+ * @param clipId - ID of the clip to delete
871
+ * @returns Dispatch result
872
+ */
873
+ rippleDelete(clipId: string): {
874
+ accepted: boolean;
875
+ errors?: {
876
+ code: string;
877
+ message: string;
878
+ }[];
879
+ };
880
+ /**
881
+ * Ripple trim - trim clip end and shift subsequent clips
882
+ *
883
+ * @param clipId - ID of the clip to trim
884
+ * @param newEnd - New end frame for the clip
885
+ * @returns Dispatch result
886
+ */
887
+ rippleTrim(clipId: string, newEnd: TimelineFrame): {
888
+ accepted: boolean;
889
+ errors?: {
890
+ code: string;
891
+ message: string;
892
+ }[];
893
+ };
894
+ /**
895
+ * Insert edit - insert clip and shift subsequent clips right
896
+ *
897
+ * @param trackId - ID of the track to insert into
898
+ * @param clip - Clip to insert
899
+ * @param atFrame - Frame to insert at
900
+ * @returns Dispatch result
901
+ */
902
+ insertEdit(trackId: string, clip: Clip, atFrame: TimelineFrame): {
903
+ accepted: boolean;
904
+ errors?: {
905
+ code: string;
906
+ message: string;
907
+ }[];
908
+ };
909
+ /**
910
+ * Ripple move - move clip and shift surrounding clips to accommodate
911
+ *
912
+ * This moves a clip to a new position while maintaining timeline continuity:
913
+ * - Closes the gap at the source position
914
+ * - Makes space at the destination position
915
+ * - All operations are atomic (single undo entry)
916
+ *
917
+ * @param clipId - ID of the clip to move
918
+ * @param newStart - New start frame for the clip
919
+ * @returns Dispatch result
920
+ */
921
+ rippleMove(clipId: string, newStart: TimelineFrame): {
922
+ accepted: boolean;
923
+ errors?: {
924
+ code: string;
925
+ message: string;
926
+ }[];
927
+ };
928
+ /**
929
+ * Insert move - move clip and shift destination clips right
930
+ *
931
+ * This moves a clip to a new position without closing the gap at source:
932
+ * - Leaves gap at the source position
933
+ * - Pushes all clips at destination right to make space
934
+ * - All operations are atomic (single undo entry)
935
+ *
936
+ * @param clipId - ID of the clip to move
937
+ * @param newStart - New start frame for the clip
938
+ * @returns Dispatch result
939
+ */
940
+ insertMove(clipId: string, newStart: TimelineFrame): {
941
+ accepted: boolean;
942
+ errors?: {
943
+ code: string;
944
+ message: string;
945
+ }[];
946
+ };
947
+ }
948
+
949
+ /**
950
+ * OPERATION PRIMITIVES — Phase 0 compliant
951
+ *
952
+ * The ONLY way to express a mutation in the engine.
953
+ * All mutations flow through: OperationPrimitive[] → Transaction → Dispatcher.
954
+ *
955
+ * RULE: Never add a new mutation function.
956
+ * Add a new type to OperationPrimitive, handle it in the Dispatcher switch,
957
+ * update the InvariantChecker, and update OPERATIONS.md.
958
+ *
959
+ * RULE: Transactions are all-or-nothing.
960
+ * If any primitive fails validation, the entire Transaction is rejected.
961
+ */
962
+
963
+ type OperationPrimitive = {
964
+ type: 'MOVE_CLIP';
965
+ clipId: ClipId;
966
+ newTimelineStart: TimelineFrame;
967
+ targetTrackId?: TrackId;
968
+ } | {
969
+ type: 'RESIZE_CLIP';
970
+ clipId: ClipId;
971
+ edge: 'start' | 'end';
972
+ newFrame: TimelineFrame;
973
+ } | {
974
+ type: 'SLICE_CLIP';
975
+ clipId: ClipId;
976
+ atFrame: TimelineFrame;
977
+ } | {
978
+ type: 'DELETE_CLIP';
979
+ clipId: ClipId;
980
+ } | {
981
+ type: 'INSERT_CLIP';
982
+ clip: Clip;
983
+ trackId: TrackId;
984
+ } | {
985
+ type: 'SET_MEDIA_BOUNDS';
986
+ clipId: ClipId;
987
+ mediaIn: TimelineFrame;
988
+ mediaOut: TimelineFrame;
989
+ } | {
990
+ type: 'SET_CLIP_ENABLED';
991
+ clipId: ClipId;
992
+ enabled: boolean;
993
+ } | {
994
+ type: 'SET_CLIP_REVERSED';
995
+ clipId: ClipId;
996
+ reversed: boolean;
997
+ } | {
998
+ type: 'SET_CLIP_SPEED';
999
+ clipId: ClipId;
1000
+ speed: number;
1001
+ } | {
1002
+ type: 'SET_CLIP_COLOR';
1003
+ clipId: ClipId;
1004
+ color: string | null;
1005
+ } | {
1006
+ type: 'SET_CLIP_NAME';
1007
+ clipId: ClipId;
1008
+ name: string | null;
1009
+ } | {
1010
+ type: 'ADD_TRACK';
1011
+ track: Track;
1012
+ } | {
1013
+ type: 'DELETE_TRACK';
1014
+ trackId: TrackId;
1015
+ } | {
1016
+ type: 'REORDER_TRACK';
1017
+ trackId: TrackId;
1018
+ newIndex: number;
1019
+ } | {
1020
+ type: 'SET_TRACK_HEIGHT';
1021
+ trackId: TrackId;
1022
+ height: number;
1023
+ } | {
1024
+ type: 'SET_TRACK_NAME';
1025
+ trackId: TrackId;
1026
+ name: string;
1027
+ } | {
1028
+ type: 'REGISTER_ASSET';
1029
+ asset: Asset;
1030
+ } | {
1031
+ type: 'UNREGISTER_ASSET';
1032
+ assetId: AssetId;
1033
+ } | {
1034
+ type: 'SET_ASSET_STATUS';
1035
+ assetId: AssetId;
1036
+ status: AssetStatus;
1037
+ } | {
1038
+ type: 'RENAME_TIMELINE';
1039
+ name: string;
1040
+ } | {
1041
+ type: 'SET_TIMELINE_DURATION';
1042
+ duration: TimelineFrame;
1043
+ } | {
1044
+ type: 'SET_TIMELINE_START_TC';
1045
+ startTimecode: Timecode;
1046
+ } | {
1047
+ type: 'SET_SEQUENCE_SETTINGS';
1048
+ settings: Partial<SequenceSettings>;
1049
+ } | {
1050
+ type: 'ADD_MARKER';
1051
+ marker: Marker;
1052
+ } | {
1053
+ type: 'MOVE_MARKER';
1054
+ markerId: MarkerId;
1055
+ newFrame: TimelineFrame;
1056
+ } | {
1057
+ type: 'DELETE_MARKER';
1058
+ markerId: MarkerId;
1059
+ } | {
1060
+ type: 'SET_IN_POINT';
1061
+ frame: TimelineFrame | null;
1062
+ } | {
1063
+ type: 'SET_OUT_POINT';
1064
+ frame: TimelineFrame | null;
1065
+ } | {
1066
+ type: 'ADD_BEAT_GRID';
1067
+ beatGrid: BeatGrid;
1068
+ } | {
1069
+ type: 'REMOVE_BEAT_GRID';
1070
+ } | {
1071
+ type: 'INSERT_GENERATOR';
1072
+ generator: Generator;
1073
+ trackId: TrackId;
1074
+ atFrame: TimelineFrame;
1075
+ } | {
1076
+ type: 'ADD_CAPTION';
1077
+ caption: Omit<Caption, 'style'> & {
1078
+ style?: CaptionStyle;
1079
+ };
1080
+ trackId: TrackId;
1081
+ } | {
1082
+ type: 'EDIT_CAPTION';
1083
+ captionId: CaptionId;
1084
+ trackId: TrackId;
1085
+ text?: string;
1086
+ language?: string;
1087
+ style?: Partial<CaptionStyle>;
1088
+ burnIn?: boolean;
1089
+ startFrame?: TimelineFrame;
1090
+ endFrame?: TimelineFrame;
1091
+ } | {
1092
+ type: 'DELETE_CAPTION';
1093
+ captionId: CaptionId;
1094
+ trackId: TrackId;
1095
+ };
1096
+ /**
1097
+ * Transaction — an atomic, labeled batch of OperationPrimitives.
1098
+ *
1099
+ * All primitives in a Transaction are validated before any are applied.
1100
+ * If one fails, none are applied. This is the all-or-nothing rule.
1101
+ */
1102
+ type Transaction = {
1103
+ readonly id: string;
1104
+ readonly label: string;
1105
+ readonly timestamp: number;
1106
+ readonly operations: readonly OperationPrimitive[];
1107
+ };
1108
+ type RejectionReason = 'OVERLAP' | 'LOCKED_TRACK' | 'ASSET_MISSING' | 'TYPE_MISMATCH' | 'OUT_OF_BOUNDS' | 'MEDIA_BOUNDS_INVALID' | 'ASSET_IN_USE' | 'TRACK_NOT_EMPTY' | 'SPEED_INVALID' | 'INVARIANT_VIOLATED' | 'NOT_FOUND' | 'BEAT_GRID_EXISTS';
1109
+ type DispatchResult = {
1110
+ accepted: true;
1111
+ nextState: TimelineState;
1112
+ } | {
1113
+ accepted: false;
1114
+ reason: RejectionReason;
1115
+ message: string;
1116
+ };
1117
+ type ViolationType = 'OVERLAP' | 'MEDIA_BOUNDS_INVALID' | 'ASSET_MISSING' | 'TRACK_TYPE_MISMATCH' | 'CLIP_BEYOND_TIMELINE' | 'TRACK_NOT_SORTED' | 'DURATION_MISMATCH' | 'SPEED_INVALID' | 'SCHEMA_VERSION_MISMATCH' | 'MARKER_OUT_OF_BOUNDS' | 'IN_OUT_INVALID' | 'BEAT_GRID_INVALID' | 'CAPTION_OUT_OF_BOUNDS' | 'CAPTION_OVERLAP';
1118
+ type InvariantViolation = {
1119
+ readonly type: ViolationType;
1120
+ readonly entityId: string;
1121
+ readonly message: string;
1122
+ };
1123
+
1124
+ /**
1125
+ * DISPATCHER — Phase 0 compliant
1126
+ *
1127
+ * The ONLY entry point for mutating TimelineState.
1128
+ * Validates first, applies atomically, checks invariants.
1129
+ *
1130
+ * Algorithm:
1131
+ * 1. For each operation: run per-primitive validator → reject immediately on failure
1132
+ * 2. Apply all operations sequentially to get proposedState
1133
+ * 3. Run checkInvariants(proposedState) → reject on any violation
1134
+ * 4. Bump timeline.version by 1 and return accepted
1135
+ *
1136
+ * RULE: If one primitive fails, zero primitives are applied.
1137
+ */
1138
+
1139
+ declare function dispatch(state: TimelineState, transaction: Transaction): DispatchResult;
1140
+
1141
+ /**
1142
+ * INVARIANT CHECKER — Phase 0 compliant
1143
+ *
1144
+ * The most critical file in the engine.
1145
+ * checkInvariants() runs after every proposed state change inside the Dispatcher.
1146
+ * Zero violations is the only acceptable result in tests and at commit time.
1147
+ *
1148
+ * RULE: Run checkInvariants in EVERY test after every state mutation.
1149
+ */
1150
+
1151
+ declare function checkInvariants(state: TimelineState): InvariantViolation[];
1152
+
1153
+ /**
1154
+ * HISTORY ENGINE
1155
+ *
1156
+ * Snapshot-based undo/redo system for timeline state.
1157
+ *
1158
+ * WHAT IS THE HISTORY ENGINE?
1159
+ * - Stores immutable snapshots of timeline state
1160
+ * - Provides undo/redo functionality
1161
+ * - Prevents state corruption
1162
+ *
1163
+ * HOW IT WORKS:
1164
+ * - past: Array of previous states
1165
+ * - present: Current state
1166
+ * - future: Array of states that can be redone
1167
+ *
1168
+ * WHY SNAPSHOTS?
1169
+ * - Simple and reliable (no complex diffing)
1170
+ * - Guaranteed to restore exact state
1171
+ * - No risk of partial corruption
1172
+ * - Easy to implement and test
1173
+ *
1174
+ * USAGE:
1175
+ * ```typescript
1176
+ * let history = createHistory(initialState);
1177
+ * history = pushHistory(history, newState);
1178
+ * history = undo(history);
1179
+ * history = redo(history);
1180
+ * ```
1181
+ *
1182
+ * ALL FUNCTIONS ARE PURE:
1183
+ * - Take history as input
1184
+ * - Return new history as output
1185
+ * - Never mutate input
1186
+ */
1187
+
1188
+ /**
1189
+ * HistoryState - The history container
1190
+ *
1191
+ * Contains:
1192
+ * - past: Array of previous states (oldest first)
1193
+ * - present: Current state
1194
+ * - future: Array of states that can be redone (newest first)
1195
+ * - limit: Maximum number of past states to keep
1196
+ */
1197
+ interface HistoryState {
1198
+ past: TimelineState[];
1199
+ present: TimelineState;
1200
+ future: TimelineState[];
1201
+ limit: number;
1202
+ }
1203
+ /**
1204
+ * Create a new history state
1205
+ *
1206
+ * @param initialState - Initial timeline state
1207
+ * @param limit - Maximum number of past states to keep (default: 50)
1208
+ * @returns A new HistoryState
1209
+ */
1210
+ declare function createHistory(initialState: TimelineState, limit?: number): HistoryState;
1211
+ /**
1212
+ * Push a new state to history
1213
+ *
1214
+ * Moves current state to past, sets new state as present,
1215
+ * and clears future (can't redo after new action).
1216
+ *
1217
+ * @param history - Current history state
1218
+ * @param newState - New timeline state to push
1219
+ * @returns New history state with new state pushed
1220
+ */
1221
+ declare function pushHistory(history: HistoryState, newState: TimelineState): HistoryState;
1222
+ /**
1223
+ * Undo the last action
1224
+ *
1225
+ * Moves current state to future, pops last state from past
1226
+ * and sets it as present.
1227
+ *
1228
+ * @param history - Current history state
1229
+ * @returns New history state with undo applied
1230
+ */
1231
+ declare function undo(history: HistoryState): HistoryState;
1232
+ /**
1233
+ * Redo the last undone action
1234
+ *
1235
+ * Moves current state to past, pops first state from future
1236
+ * and sets it as present.
1237
+ *
1238
+ * @param history - Current history state
1239
+ * @returns New history state with redo applied
1240
+ */
1241
+ declare function redo(history: HistoryState): HistoryState;
1242
+ /**
1243
+ * Check if undo is available
1244
+ *
1245
+ * @param history - Current history state
1246
+ * @returns true if undo is available
1247
+ */
1248
+ declare function canUndo(history: HistoryState): boolean;
1249
+ /**
1250
+ * Check if redo is available
1251
+ *
1252
+ * @param history - Current history state
1253
+ * @returns true if redo is available
1254
+ */
1255
+ declare function canRedo(history: HistoryState): boolean;
1256
+ /**
1257
+ * Get the current state from history
1258
+ *
1259
+ * @param history - Current history state
1260
+ * @returns The current timeline state
1261
+ */
1262
+ declare function getCurrentState(history: HistoryState): TimelineState;
1263
+
1264
+ /**
1265
+ * SNAP INDEX — Phase 1
1266
+ *
1267
+ * Pure functions. Zero React/DOM imports. Zero mutation.
1268
+ *
1269
+ * Phase 1 snap sources: ClipStart, ClipEnd, Playhead.
1270
+ * Phase 2 will add: Marker, InPoint, OutPoint.
1271
+ * Phase 3 will add: BeatGrid.
1272
+ *
1273
+ * Priority table (do not change values):
1274
+ * Marker: 100
1275
+ * InPoint: 90
1276
+ * OutPoint: 90
1277
+ * ClipStart: 80
1278
+ * ClipEnd: 80
1279
+ * Playhead: 70
1280
+ * BeatGrid: 50
1281
+ */
1282
+
1283
+ /**
1284
+ * All snap point sources across phases.
1285
+ * Defined in full now so SnapPoint & allowedTypes filters are stable.
1286
+ */
1287
+ type SnapPointType = 'ClipStart' | 'ClipEnd' | 'Playhead' | 'Marker' | 'InPoint' | 'OutPoint' | 'BeatGrid';
1288
+ type SnapPoint = {
1289
+ readonly frame: TimelineFrame;
1290
+ readonly type: SnapPointType;
1291
+ readonly priority: number;
1292
+ readonly trackId: TrackId | null;
1293
+ readonly sourceId: string;
1294
+ };
1295
+ type SnapIndex = {
1296
+ readonly points: readonly SnapPoint[];
1297
+ readonly builtAt: number;
1298
+ readonly enabled: boolean;
1299
+ };
1300
+ /**
1301
+ * Build a SnapIndex from committed state + playhead position.
1302
+ *
1303
+ * RULE: Call via queueMicrotask after accepted dispatch.
1304
+ * Never call during a drag (pointer move).
1305
+ *
1306
+ * Phase 1 sources pulled (in order):
1307
+ * 1. ClipStart + ClipEnd from every clip on every track
1308
+ * 2. Playhead position (trackId = null)
1309
+ */
1310
+ declare function buildSnapIndex(state: TimelineState, playheadFrame: TimelineFrame, enabled?: boolean): SnapIndex;
1311
+ /**
1312
+ * Find the highest-priority snap candidate within radiusFrames.
1313
+ *
1314
+ * Returns null when:
1315
+ * - index.enabled is false
1316
+ * - no point is within radiusFrames of frame
1317
+ *
1318
+ * Tiebreak (equidistant candidates): highest priority wins.
1319
+ * Second tiebreak (equal priority): first in sorted order.
1320
+ *
1321
+ * @param exclude sourceIds to skip (e.g. the clip being dragged)
1322
+ * @param allowedTypes if provided, only consider points of these types
1323
+ */
1324
+ declare function nearest(index: SnapIndex, frame: TimelineFrame, radiusFrames: number, exclude?: readonly string[], allowedTypes?: readonly SnapPointType[]): SnapPoint | null;
1325
+ /**
1326
+ * Return a new SnapIndex with enabled toggled.
1327
+ * Does NOT rebuild points — pure field update.
1328
+ */
1329
+ declare function toggleSnap(index: SnapIndex, enabled: boolean): SnapIndex;
1330
+
1331
+ /**
1332
+ * TOOL CONTRACT TYPES — Phase 1
1333
+ *
1334
+ * Zero implementation. Zero imports from React or DOM.
1335
+ * Every ITool must satisfy this interface exactly.
1336
+ *
1337
+ * RULES (from ITOOL_CONTRACT.md):
1338
+ * - onPointerMove NEVER calls dispatch
1339
+ * - onPointerUp NEVER mutates instance state
1340
+ * - onKeyDown, onKeyUp, onCancel are REQUIRED — implement as no-ops if unused
1341
+ */
1342
+
1343
+ type ToolId = string & {
1344
+ readonly __brand: 'ToolId';
1345
+ };
1346
+ declare function toToolId(s: string): ToolId;
1347
+ /** Keyboard modifier state — available on ToolContext so getCursor() can
1348
+ * react to held keys even when no pointer event is firing. */
1349
+ type Modifiers = {
1350
+ readonly shift: boolean;
1351
+ readonly alt: boolean;
1352
+ readonly ctrl: boolean;
1353
+ readonly meta: boolean;
1354
+ };
1355
+ /** Normalised pointer event in frame-space.
1356
+ * ToolRouter populates clipId via hit-test — tools never recompute it. */
1357
+ type TimelinePointerEvent = {
1358
+ readonly frame: TimelineFrame;
1359
+ readonly trackId: TrackId | null;
1360
+ readonly clipId: ClipId | null;
1361
+ readonly x: number;
1362
+ readonly y: number;
1363
+ readonly buttons: number;
1364
+ readonly shiftKey: boolean;
1365
+ readonly altKey: boolean;
1366
+ readonly metaKey: boolean;
1367
+ };
1368
+ type TimelineKeyEvent = {
1369
+ readonly key: string;
1370
+ readonly code: string;
1371
+ readonly shiftKey: boolean;
1372
+ readonly altKey: boolean;
1373
+ readonly metaKey: boolean;
1374
+ };
1375
+ /** Pixel + frame region swept by a rubber-band (marquee) selection drag.
1376
+ * Populated by SelectionTool during rubber-band drags. */
1377
+ type RubberBandRegion = {
1378
+ readonly startFrame: TimelineFrame;
1379
+ readonly endFrame: TimelineFrame;
1380
+ readonly startY: number;
1381
+ readonly endY: number;
1382
+ };
1383
+ /** Ghost state produced by onPointerMove.
1384
+ * isProvisional: true is a compile-time discriminant so resolveClip()
1385
+ * can distinguish provisional from committed Clip[] arrays. */
1386
+ type ProvisionalState = {
1387
+ readonly clips: readonly Clip[];
1388
+ readonly rubberBand?: RubberBandRegion;
1389
+ readonly isProvisional: true;
1390
+ };
1391
+ /** Injected by TimelineEngine on every event call.
1392
+ * Tools never import TimelineEngine. They never call dispatch() directly. */
1393
+ type ToolContext = {
1394
+ readonly state: TimelineState;
1395
+ readonly snapIndex: SnapIndex;
1396
+ readonly pixelsPerFrame: number;
1397
+ /** Current modifier key state — updates on every pointer/key event. */
1398
+ readonly modifiers: Modifiers;
1399
+ /** Convert a client-pixel x-position to a TimelineFrame. */
1400
+ readonly frameAtX: (x: number) => TimelineFrame;
1401
+ /** Return the TrackId whose row contains client-pixel y, or null. */
1402
+ readonly trackAtY: (y: number) => TrackId | null;
1403
+ /** Query snap and return the snapped frame (or original if no hit).
1404
+ * Handles enabled/disabled, radius, exclusion, and type filter internally.
1405
+ * Tools never see radiusFrames or the enabled flag. */
1406
+ readonly snap: (frame: TimelineFrame, exclude?: readonly string[], allowedTypes?: readonly SnapPointType[]) => TimelineFrame;
1407
+ };
1408
+ interface ITool {
1409
+ readonly id: ToolId;
1410
+ /** Single-character keyboard shortcut, e.g. 'v', 'b', 'r'. Empty string = no shortcut. */
1411
+ readonly shortcutKey: string;
1412
+ /** Return the CSS cursor string for the current tool + modifier state.
1413
+ * Called on every pointermove — must be cheap. */
1414
+ getCursor(ctx: ToolContext): string;
1415
+ /** Return the SnapPointType categories this tool snaps to.
1416
+ * Used by ctx.snap() to filter the snap index automatically. */
1417
+ getSnapCandidateTypes(): readonly SnapPointType[];
1418
+ onPointerDown(event: TimelinePointerEvent, ctx: ToolContext): void;
1419
+ /** Return ProvisionalState for ghost rendering.
1420
+ * MUST NOT call dispatch. MUST NOT call engine methods. */
1421
+ onPointerMove(event: TimelinePointerEvent, ctx: ToolContext): ProvisionalState | null;
1422
+ /** Return a Transaction to commit, or null if this gesture produces no edit.
1423
+ * MUST NOT mutate any instance state. */
1424
+ onPointerUp(event: TimelinePointerEvent, ctx: ToolContext): Transaction | null;
1425
+ /** Handle a keydown — return a Transaction or null.
1426
+ * Required — implement as `return null` if unused. */
1427
+ onKeyDown(event: TimelineKeyEvent, ctx: ToolContext): Transaction | null;
1428
+ /** Handle a keyup — no return value.
1429
+ * Required — implement as no-op if unused. */
1430
+ onKeyUp(event: TimelineKeyEvent, ctx: ToolContext): void;
1431
+ /** Called when a gesture is interrupted (Escape, tool switch mid-drag).
1432
+ * Required — implement as no-op if unused. */
1433
+ onCancel(): void;
1434
+ }
1435
+
1436
+ /**
1437
+ * TOOL REGISTRY — Phase 1
1438
+ *
1439
+ * Pure functions. No classes. No React. No state mutation.
1440
+ *
1441
+ * ToolRegistry is immutable data — activateTool returns a NEW registry.
1442
+ * The active tool lives here, not on TimelineEngine, keeping the engine thin.
1443
+ *
1444
+ * RULES:
1445
+ * - activateTool calls outgoing.onCancel() before switching
1446
+ * - activateTool throws on unknown id (programmer error, never user error)
1447
+ * - NoOpTool is the canonical do-nothing ITool (test double + startup default)
1448
+ */
1449
+
1450
+ type ToolRegistry = {
1451
+ readonly tools: ReadonlyMap<ToolId, ITool>;
1452
+ readonly activeToolId: ToolId;
1453
+ };
1454
+ /**
1455
+ * Create an initial registry from an array of tools.
1456
+ *
1457
+ * @throws if defaultId is not present in the tools array
1458
+ */
1459
+ declare function createRegistry(tools: readonly ITool[], defaultId: ToolId): ToolRegistry;
1460
+ /**
1461
+ * Activate a new tool.
1462
+ *
1463
+ * Steps (must run in order):
1464
+ * 1. Call outgoing tool's onCancel() — cleans up any in-progress drag state
1465
+ * 2. Validate that the new id exists in the registry
1466
+ * 3. Return a new ToolRegistry with activeToolId updated
1467
+ *
1468
+ * @throws if id is not registered
1469
+ */
1470
+ declare function activateTool(registry: ToolRegistry, id: ToolId): ToolRegistry;
1471
+ /**
1472
+ * Return the currently active ITool.
1473
+ * Never returns undefined — registry invariant guarantees activeToolId is registered.
1474
+ */
1475
+ declare function getActiveTool(registry: ToolRegistry): ITool;
1476
+ /**
1477
+ * Return a new registry with the tool added.
1478
+ * If a tool with the same id already exists, it is replaced.
1479
+ * activeToolId is unchanged.
1480
+ */
1481
+ declare function registerTool(registry: ToolRegistry, tool: ITool): ToolRegistry;
1482
+ /**
1483
+ * Satisfies ITool with no side effects.
1484
+ *
1485
+ * Use for:
1486
+ * - Test doubles (spread and override only the methods you need)
1487
+ * - Default active tool on engine startup
1488
+ * - ToolRouter smoke tests
1489
+ *
1490
+ * onCancel() is a deliberate no-op: NoOpTool has no drag state to clean up.
1491
+ * Real tools will clear instance variables there.
1492
+ */
1493
+ declare const NoOpTool: ITool;
1494
+
1495
+ /**
1496
+ * PROVISIONAL MANAGER — Phase 1
1497
+ *
1498
+ * Manages ghost state during pointer drags.
1499
+ *
1500
+ * RULES (from ITOOL_CONTRACT.md):
1501
+ * - setProvisional / clearProvisional return NEW objects — never mutate
1502
+ * - resolveClip checks provisional first, then committed state
1503
+ * - The engine calls clearProvisional() BEFORE dispatching onPointerUp's tx
1504
+ * - Provisional updates trigger notify() so ghosts render immediately
1505
+ *
1506
+ * resolveClip priority:
1507
+ * 1. provisional.clips has a clip with this id → return ghost version
1508
+ * 2. clip exists in committed state → return committed
1509
+ * 3. clip absent from both (deleted mid-drag) → return undefined
1510
+ */
1511
+
1512
+ type ProvisionalManager = {
1513
+ readonly current: ProvisionalState | null;
1514
+ };
1515
+ /** Create an empty provisional manager (current = null). */
1516
+ declare function createProvisionalManager(): ProvisionalManager;
1517
+ /** Return a new manager with current set to state.
1518
+ * Pure — never mutates the original manager. */
1519
+ declare function setProvisional(_manager: ProvisionalManager, state: ProvisionalState): ProvisionalManager;
1520
+ /** Return a new manager with current set to null.
1521
+ * Pure — never mutates the original manager. */
1522
+ declare function clearProvisional(_manager: ProvisionalManager): ProvisionalManager;
1523
+ /**
1524
+ * Resolve which version of a clip to render.
1525
+ *
1526
+ * Priority:
1527
+ * 1. If manager.current has a clip with this id → return provisional (ghost)
1528
+ * 2. Otherwise → search committed state
1529
+ * 3. If absent from both (clip deleted mid-drag) → return undefined
1530
+ *
1531
+ * Returns undefined if the clip has been deleted from committed state
1532
+ * and is not in provisional. Components must handle this:
1533
+ * const clip = useClip(id)
1534
+ * if (!clip) return null ← required, not optional
1535
+ *
1536
+ * Call site in useClip selector:
1537
+ * () => resolveClip(id, engine.getSnapshot(), engine.getProvisionalManager())
1538
+ */
1539
+ declare function resolveClip(clipId: ClipId, state: TimelineState, manager: ProvisionalManager): Clip | undefined;
1540
+
1541
+ /**
1542
+ * MARKER SEARCH API — Phase 3 Step 2
1543
+ *
1544
+ * Pure functions. Search state.timeline.markers only.
1545
+ */
1546
+
1547
+ /**
1548
+ * Returns markers whose color exactly matches the given string.
1549
+ */
1550
+ declare function findMarkersByColor(state: TimelineState, color: string): Marker[];
1551
+ /**
1552
+ * Returns markers whose label contains the given string (case-insensitive).
1553
+ */
1554
+ declare function findMarkersByLabel(state: TimelineState, label: string): Marker[];
1555
+
1556
+ /**
1557
+ * SUBTITLE IMPORT — Phase 3 Step 3
1558
+ *
1559
+ * Pure functions for parsing SRT/VTT into Caption[].
1560
+ * No file IO. No DOM. No external deps.
1561
+ */
1562
+
1563
+ declare const defaultCaptionStyle: CaptionStyle;
1564
+ type SRTParseOptions = {
1565
+ language?: string;
1566
+ burnIn?: boolean;
1567
+ defaultStyle?: Partial<CaptionStyle>;
1568
+ };
1569
+ type VTTParseOptions = SRTParseOptions;
1570
+ declare function parseSRT(raw: string, fps: number, options?: SRTParseOptions): Caption[];
1571
+ declare function parseVTT(raw: string, fps: number, options?: VTTParseOptions): Caption[];
1572
+ declare function subtitleImportToOps(captions: Caption[], trackId: TrackId): OperationPrimitive[];
1573
+
1574
+ export { createAsset as $, type Asset as A, type TrackType as B, type Clip as C, type DispatchResult as D, type Transaction as E, type FrameRate as F, type ViolationType as G, type HistoryState as H, type ITool as I, activateTool as J, addFrames as K, buildSnapIndex as L, type Modifiers as M, NoOpTool as N, type OperationPrimitive as O, type ProvisionalManager as P, canRedo as Q, type RationalTime as R, type SRTParseOptions as S, type TimelineState as T, canUndo as U, type VTTParseOptions as V, checkInvariants as W, clampFrame as X, clearProvisional as Y, clipContainsFrame as Z, clipsOverlap as _, type Track as a, createClip as a0, createHistory as a1, createProvisionalManager as a2, createRegistry as a3, createTimeline as a4, createTimelineState as a5, createTrack as a6, defaultCaptionStyle as a7, dispatch as a8, findMarkersByColor as a9, toClipId as aA, toFrame as aB, toTimecode as aC, toToolId as aD, toTrackId as aE, toggleSnap as aF, undo as aG, findMarkersByLabel as aa, frame as ab, frameDuration as ac, frameRate as ad, framesToMinutesSeconds as ae, framesToSeconds as af, framesToTimecode as ag, getActiveTool as ah, getClipDuration as ai, getClipMediaDuration as aj, getCurrentState as ak, isDropFrame as al, isValidFrame as am, nearest as an, parseSRT as ao, parseVTT as ap, pushHistory as aq, redo as ar, registerTool as as, resolveClip as at, secondsToFrames as au, setProvisional as av, sortTrackClips as aw, subtitleImportToOps as ax, subtractFrames as ay, toAssetId as az, type TimelineFrame as b, type AssetId as c, type AssetRegistry as d, type AssetStatus as e, CURRENT_SCHEMA_VERSION as f, type ClipId as g, FrameRates as h, type InvariantViolation as i, type ProvisionalState as j, type RejectionReason as k, type RubberBandRegion as l, type SequenceSettings as m, type SnapIndex as n, type SnapPoint as o, type SnapPointType as p, type TimeRange as q, type Timecode as r, type Timeline as s, TimelineEngine as t, type TimelineKeyEvent as u, type TimelinePointerEvent as v, type ToolContext as w, type ToolId as x, type ToolRegistry as y, type TrackId as z };