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