@aicut/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,640 @@
1
+ /**
2
+ * UI strings the editor paints into the DOM (toolbar tooltips, the
3
+ * fullscreen exit button) and onto the timeline canvas (phantom new-
4
+ * track label, track header labels). Every user-visible literal in
5
+ * `@aicut/core` flows through this interface — there are no hidden
6
+ * hard-coded translations elsewhere in the library.
7
+ *
8
+ * Defaults to English. Hosts that want Chinese (or any other locale)
9
+ * pass `locale: localeZh` to `Editor.create` / `Timeline.create`, or
10
+ * override individual keys with `locale: { undo: "撤销" }`.
11
+ */
12
+ interface Locale {
13
+ undo: string;
14
+ redo: string;
15
+ split: string;
16
+ trimLeft: string;
17
+ trimRight: string;
18
+ speedComingSoon: string;
19
+ playPause: string;
20
+ fullscreen: string;
21
+ snap: string;
22
+ /** Title shown on the snap button when snap is ON (clicking turns OFF). */
23
+ snapOnTitle: string;
24
+ /** Title shown when snap is OFF (clicking turns ON). */
25
+ snapOffTitle: string;
26
+ zoomOut: string;
27
+ zoomIn: string;
28
+ reset: string;
29
+ exitFullscreen: string;
30
+ exitFullscreenTitle: string;
31
+ /** Phantom row that appears under the last track during a drag. */
32
+ newTrack: string;
33
+ /** Track header — `{n}` is replaced with the 1-based track index. */
34
+ videoTrackLabel: string;
35
+ /** Same template format as videoTrackLabel. */
36
+ audioTrackLabel: string;
37
+ }
38
+ /** English. The library default — chosen over Chinese as the OSS norm. */
39
+ declare const localeEn: Locale;
40
+ /** Simplified Chinese. */
41
+ declare const localeZh: Locale;
42
+ /** Spread defaults under host overrides — host can supply a partial. */
43
+ declare function mergeLocale(partial: Partial<Locale> | undefined): Locale;
44
+ /**
45
+ * Replace `{key}` placeholders in a template. We only need `{n}`
46
+ * substitution today; the implementation is generic so additional
47
+ * keys (e.g. `{name}`) won't need a second pass.
48
+ */
49
+ declare function formatLabel(template: string, vars: Record<string, string | number>): string;
50
+
51
+ /**
52
+ * Milliseconds. All timing in the project is expressed as integer ms to
53
+ * keep JSON serialization unambiguous (no frame-rate coupling in the
54
+ * data model — the renderer can present time as frames if it wants).
55
+ */
56
+ type Ms = number;
57
+ interface MediaSource {
58
+ id: string;
59
+ url: string;
60
+ kind: "video" | "audio";
61
+ /** Optional — probed lazily from the <video> element if absent. */
62
+ duration?: Ms;
63
+ name?: string;
64
+ }
65
+ interface Clip {
66
+ id: string;
67
+ sourceId: string;
68
+ /** Window into the source — `in` inclusive, `out` exclusive. */
69
+ in: Ms;
70
+ out: Ms;
71
+ /** Position on the timeline. */
72
+ start: Ms;
73
+ /**
74
+ * Playback rate. 1 = normal, 2 = 2× speed. Default 1.
75
+ * Persisted in the project JSON so a host can restore exactly.
76
+ */
77
+ speed?: number;
78
+ }
79
+ interface Track {
80
+ id: string;
81
+ kind: "video" | "audio";
82
+ /** Clips on this track. Must be kept sorted by `start` and non-overlapping. */
83
+ clips: Clip[];
84
+ }
85
+ interface Project {
86
+ /** Schema version — bump when breaking the JSON shape. */
87
+ version: 1;
88
+ sources: MediaSource[];
89
+ tracks: Track[];
90
+ }
91
+ /**
92
+ * Subset of CSS variables the editor honors. Pass any custom values
93
+ * via `Editor` options; everything is forwarded as `--aicut-*` on the
94
+ * editor's root container, so a host can also override via plain CSS.
95
+ */
96
+ interface Theme {
97
+ brand?: string;
98
+ secondary?: string;
99
+ surface?: string;
100
+ dark?: string;
101
+ muted?: string;
102
+ card?: string;
103
+ success?: string;
104
+ warning?: string;
105
+ info?: string;
106
+ error?: string;
107
+ /** Toolbar / ruler chrome. Background of the editor frame. */
108
+ controlsBg?: string;
109
+ controlsBorder?: string;
110
+ controlsText?: string;
111
+ controlsHover?: string;
112
+ controlsActive?: string;
113
+ /** Letterbox color around the preview video. Defaults to black. */
114
+ previewBg?: string;
115
+ radiusSm?: string;
116
+ radiusMd?: string;
117
+ radiusLg?: string;
118
+ }
119
+
120
+ interface EditorOptions {
121
+ /** Host element to mount the editor into. Will be wiped on init. */
122
+ container: HTMLElement;
123
+ /** Initial project state. Falls back to an empty single-track project. */
124
+ project?: Project;
125
+ /** CSS variable overrides. */
126
+ theme?: Theme;
127
+ /** Initial playhead position (ms). */
128
+ initialTime?: Ms;
129
+ /** Initial timeline zoom (pixels per second). Defaults to 80. */
130
+ initialScale?: number;
131
+ /** Initial snap toggle. Defaults to true. */
132
+ initialSnap?: boolean;
133
+ /**
134
+ * UI string overrides. Falls back to English (`localeEn`) for any
135
+ * keys not provided. Use `localeZh` as the value for full Chinese.
136
+ * Call `editor.setLocale(...)` to switch at runtime.
137
+ */
138
+ locale?: Partial<Locale>;
139
+ }
140
+ interface EditorEventMap {
141
+ /** Emitted whenever the project mutates. */
142
+ change: {
143
+ project: Project;
144
+ };
145
+ /** Playhead position update — driven by the playback tick loop. */
146
+ time: {
147
+ timeMs: Ms;
148
+ };
149
+ play: void;
150
+ pause: void;
151
+ /** A source's metadata finished loading (duration etc). */
152
+ ready: {
153
+ sourceId: string | null;
154
+ };
155
+ /** User clicked the Export button. Host decides what to do with the JSON. */
156
+ export: {
157
+ project: Project;
158
+ };
159
+ error: {
160
+ error: Error;
161
+ };
162
+ /** Currently selected clip id, or null. */
163
+ selectionChange: {
164
+ clipId: string | null;
165
+ };
166
+ /** Zoom (px/sec) changed. */
167
+ scaleChange: {
168
+ pxPerSec: number;
169
+ };
170
+ /** Snap toggle changed. */
171
+ snapChange: {
172
+ snap: boolean;
173
+ };
174
+ /** Undo/redo stack states changed (button enablement). */
175
+ historyChange: {
176
+ canUndo: boolean;
177
+ canRedo: boolean;
178
+ };
179
+ }
180
+ type EditorEventName = keyof EditorEventMap;
181
+ interface EditorApi {
182
+ play(): void;
183
+ pause(): void;
184
+ togglePlay(): void;
185
+ seek(timeMs: Ms): void;
186
+ getTime(): Ms;
187
+ getDuration(): Ms;
188
+ isPlaying(): boolean;
189
+ enterFullscreen(): Promise<void>;
190
+ exitFullscreen(): Promise<void>;
191
+ isFullscreen(): boolean;
192
+ split(timeMs?: Ms): string[] | null;
193
+ /** Alias of split, kept for back-compat. */
194
+ cut(timeMs?: Ms): string[] | null;
195
+ /** Trim the selected clip's left edge to the given time (or playhead). */
196
+ trimLeft(timeMs?: Ms): boolean;
197
+ /** Trim the selected clip's right edge to the given time (or playhead). */
198
+ trimRight(timeMs?: Ms): boolean;
199
+ removeClip(clipId: string): boolean;
200
+ setClipSpeed(clipId: string, speed: number): boolean;
201
+ previewMoveTarget(clipId: string, start: Ms, intendedTrackId?: string): {
202
+ trackIndex: number;
203
+ trackId: string;
204
+ wouldCreateNew: boolean;
205
+ } | null;
206
+ addTrack(kind: Track["kind"]): Track;
207
+ removeTrack(trackId: string): boolean;
208
+ moveClip(clipId: string, opts: {
209
+ start?: Ms;
210
+ trackId?: string;
211
+ newTrack?: boolean;
212
+ }): boolean;
213
+ resizeClip(clipId: string, edits: Partial<Pick<Clip, "in" | "out" | "start">>): boolean;
214
+ addSource(source: MediaSource, opts?: {
215
+ appendClip?: boolean;
216
+ }): MediaSource;
217
+ setProject(project: Project): void;
218
+ getProject(): Project;
219
+ /** Replace the project with a brand-new empty one. */
220
+ reset(): void;
221
+ setTheme(theme: Theme): void;
222
+ /**
223
+ * Swap the UI locale at runtime. Partial overrides merge with the
224
+ * English default. Triggers a re-render so the toolbar tooltips
225
+ * and timeline canvas labels pick up the new strings immediately.
226
+ */
227
+ setLocale(locale: Partial<Locale>): void;
228
+ /**
229
+ * Fire the `export` event with the current project JSON. Hosts call
230
+ * this from their own export button (built into their toolbarRight
231
+ * slot, a keyboard shortcut, a menu item, etc.) to surface project
232
+ * data to whatever pipeline they own. The library never invokes
233
+ * this on its own — it has no UI for export.
234
+ */
235
+ requestExport(): void;
236
+ getScale(): number;
237
+ setScale(pxPerSec: number): void;
238
+ getSnap(): boolean;
239
+ setSnap(snap: boolean): void;
240
+ getSelection(): string | null;
241
+ setSelection(clipId: string | null): void;
242
+ canUndo(): boolean;
243
+ canRedo(): boolean;
244
+ undo(): boolean;
245
+ redo(): boolean;
246
+ /**
247
+ * Bookend slot at the very left of the top toolbar — host appends
248
+ * its own controls (e.g. an aspect-ratio dropdown). Empty by default
249
+ * and renders no separator until populated.
250
+ */
251
+ readonly toolbarLeft: HTMLElement;
252
+ /** Right-side bookend slot — conventionally export / save / share. */
253
+ readonly toolbarRight: HTMLElement;
254
+ on<K extends EditorEventName>(event: K, handler: (payload: EditorEventMap[K]) => void): () => void;
255
+ off<K extends EditorEventName>(event: K, handler: (payload: EditorEventMap[K]) => void): void;
256
+ destroy(): void;
257
+ }
258
+ /**
259
+ * Top-level editor instance — the only stateful object a host app
260
+ * interacts with. Owns the project, the playback engine, the vanilla
261
+ * DOM UI, plus viewport (zoom/snap), selection, and history state.
262
+ *
263
+ * Framework wrappers (`@aicut/react`, `@aicut/vue`) should mount a
264
+ * container, instantiate this once, mirror prop changes (`theme`)
265
+ * into it, and forward events as framework-native callbacks.
266
+ */
267
+ declare class Editor implements EditorApi {
268
+ private container;
269
+ private project;
270
+ private engine;
271
+ private ui;
272
+ private bus;
273
+ private history;
274
+ private selectedClipId;
275
+ private pxPerSec;
276
+ private snap;
277
+ private locale;
278
+ private destroyed;
279
+ constructor(opts: EditorOptions);
280
+ static create(opts: EditorOptions): Editor;
281
+ get toolbarLeft(): HTMLElement;
282
+ get toolbarRight(): HTMLElement;
283
+ play(): void;
284
+ pause(): void;
285
+ togglePlay(): void;
286
+ seek(timeMs: Ms): void;
287
+ getTime(): Ms;
288
+ getDuration(): Ms;
289
+ isPlaying(): boolean;
290
+ /**
291
+ * In-tab "fullscreen" — covers the browser viewport via fixed
292
+ * positioning, NOT the OS Fullscreen API. This is what the reference
293
+ * UI calls "全屏预览": the user stays in their tab, no browser
294
+ * permission prompt, ESC exits. Browser fullscreen would also work
295
+ * but is heavier UX and gets blocked in iframes.
296
+ */
297
+ enterFullscreen(): Promise<void>;
298
+ exitFullscreen(): Promise<void>;
299
+ isFullscreen(): boolean;
300
+ /**
301
+ * Split the clip at `timeMs` (or playhead). Returns the two new clip ids
302
+ * or null if there's no clip to split at that time.
303
+ */
304
+ split(timeMs?: Ms): string[] | null;
305
+ cut(timeMs?: Ms): string[] | null;
306
+ trimLeft(timeMs?: Ms): boolean;
307
+ trimRight(timeMs?: Ms): boolean;
308
+ removeClip(clipId: string): boolean;
309
+ setClipSpeed(clipId: string, speed: number): boolean;
310
+ addTrack(kind: Track["kind"]): Track;
311
+ removeTrack(trackId: string): boolean;
312
+ /**
313
+ * Pure prediction of where a `moveClip(...)` would land — same smart
314
+ * routing as the real move (intended → source → other → new track),
315
+ * just no mutation, no history. Lets the Timeline preview the
316
+ * ACTUAL outcome of a drop so the ghost stops lying about new
317
+ * tracks that won't get created.
318
+ */
319
+ previewMoveTarget(clipId: string, start: Ms, intendedTrackId?: string): {
320
+ trackIndex: number;
321
+ trackId: string;
322
+ wouldCreateNew: boolean;
323
+ } | null;
324
+ moveClip(clipId: string, opts: {
325
+ start?: Ms;
326
+ trackId?: string;
327
+ newTrack?: boolean;
328
+ }): boolean;
329
+ resizeClip(clipId: string, edits: Partial<Pick<Clip, "in" | "out" | "start">>): boolean;
330
+ addSource(source: MediaSource, opts?: {
331
+ appendClip?: boolean;
332
+ }): MediaSource;
333
+ setProject(project: Project): void;
334
+ getProject(): Project;
335
+ /**
336
+ * Restore the "fresh import" state: same media library, single
337
+ * default video track, one full-length clip per video source laid
338
+ * end-to-end. This mirrors the initial layout a host would get
339
+ * after dropping their videos in, so "reset" feels like "start
340
+ * over without re-importing" rather than "wipe everything".
341
+ *
342
+ * Goes through the regular history stack — ⌘Z brings the previous
343
+ * edit back. Sources without a known duration are skipped (they'd
344
+ * render as zero-width clips, which is worse than absent).
345
+ */
346
+ reset(): void;
347
+ setTheme(theme: Theme): void;
348
+ setLocale(locale: Partial<Locale>): void;
349
+ /** Internal — UI reads the resolved locale here on each render. */
350
+ getLocale(): Locale;
351
+ requestExport(): void;
352
+ getScale(): number;
353
+ setScale(pxPerSec: number): void;
354
+ getSnap(): boolean;
355
+ setSnap(snap: boolean): void;
356
+ /** Snap a candidate ms to the nearest snappable surface within SNAP_PX. */
357
+ snapMs(timeMs: Ms, ignoreClipId?: string | null): Ms;
358
+ getSelection(): string | null;
359
+ setSelection(clipId: string | null): void;
360
+ canUndo(): boolean;
361
+ canRedo(): boolean;
362
+ undo(): boolean;
363
+ redo(): boolean;
364
+ on<K extends EditorEventName>(event: K, handler: (payload: EditorEventMap[K]) => void): () => void;
365
+ off<K extends EditorEventName>(event: K, handler: (payload: EditorEventMap[K]) => void): void;
366
+ destroy(): void;
367
+ private appendTrack;
368
+ private resolveTrimTarget;
369
+ private pushHistory;
370
+ private emitHistory;
371
+ private afterMutation;
372
+ private handleSourceMetadata;
373
+ }
374
+
375
+ declare function createEmptyProject(): Project;
376
+ /**
377
+ * Defensive normalization — ensures clips on each track are sorted by
378
+ * `start`, IDs exist, and trivially-empty clips (out <= in) are dropped.
379
+ * Called from `Editor.setProject` so consumers can hand us loosely-formed
380
+ * JSON without risking inconsistent internal state.
381
+ */
382
+ declare function normalizeProject(project: Project): Project;
383
+
384
+ /**
385
+ * Short, stable, URL-safe ID. Not cryptographic — fine for client IDs
386
+ * that get persisted in the project JSON.
387
+ */
388
+ declare function createId(prefix?: string): string;
389
+
390
+ /** Visual constants — kept here so draw + hit-test share one source of truth. */
391
+ declare const TRACK_HEIGHT = 56;
392
+ declare const RULER_HEIGHT = 24;
393
+ declare const HEADER_WIDTH = 96;
394
+
395
+ /**
396
+ * Public options for the standalone `Timeline` component. The class
397
+ * is framework-agnostic — `@aicut/react` and `@aicut/vue` wrap it,
398
+ * and the built-in `Editor` composes one internally for its timeline
399
+ * panel. Reuse the same instance for a "frame-picker" use case by
400
+ * loading a project with a single video clip and `readOnly: true`.
401
+ */
402
+ interface TimelineOptions {
403
+ /** Host element. Will be wiped on init. */
404
+ container: HTMLElement;
405
+ project: Project;
406
+ /** Pixels per second. Defaults to 80; auto-fits on mount when possible. */
407
+ pxPerSec?: number;
408
+ /** Initial playhead position. */
409
+ time?: Ms;
410
+ /** Initially selected clip. */
411
+ selectedClipId?: string | null;
412
+ /** Show the track-name header column (left). Default true. */
413
+ showHeader?: boolean;
414
+ /** Disable interactions — useful for read-only preview / frame picker. */
415
+ readOnly?: boolean;
416
+ /** Snap to clip edges + playhead when dragging. Default true. */
417
+ snap?: boolean;
418
+ /** Compute and apply fit-to-window on first project change. Default true. */
419
+ autoFit?: boolean;
420
+ /** UI string overrides (English defaults). Use `localeZh` for Chinese. */
421
+ locale?: Partial<Locale>;
422
+ /**
423
+ * Render an empty 36px toolbar strip at the top of the host element
424
+ * with `toolbarLeft` / `toolbarRight` flex slots. The library paints
425
+ * NOTHING into either slot — hosts append their own controls (an
426
+ * export button, a size/aspect dropdown, etc.). Default false; the
427
+ * canvas takes the full host height when the toolbar is off, so
428
+ * adopting the slot later is a non-breaking opt-in.
429
+ */
430
+ toolbar?: boolean;
431
+ onSeek?: (timeMs: Ms) => void;
432
+ onSelectClip?: (clipId: string | null) => void;
433
+ onScaleChange?: (pxPerSec: number) => void;
434
+ onDeleteTrack?: (trackId: string) => void;
435
+ onMoveClip?: (clipId: string, opts: {
436
+ start?: Ms;
437
+ trackId?: string;
438
+ newTrack?: boolean;
439
+ }) => void;
440
+ onResizeClip?: (clipId: string, edits: Partial<Pick<Clip, "in" | "out" | "start">>) => void;
441
+ onChange?: (project: Project) => void;
442
+ /**
443
+ * Lets the host predict where a drop will actually land — used to
444
+ * keep the drag-ghost visual honest. The Editor wires this to its
445
+ * smart routing (intended → source → other → new track), so the
446
+ * ghost shows the real outcome rather than just the user's hover.
447
+ *
448
+ * Return `{ trackIndex }` for an existing track, or
449
+ * `{ wouldCreateNew: true }` for the auto-split case.
450
+ */
451
+ resolveDrop?: (clipId: string, intent: {
452
+ start: Ms;
453
+ intendedTrackIndex: number;
454
+ }) => {
455
+ trackIndex: number;
456
+ wouldCreateNew: boolean;
457
+ };
458
+ }
459
+ /**
460
+ * Canvas-rendered, framework-free timeline. Owns ruler, multi-track
461
+ * layout, headers, frame-thumbnails, playhead, snap, and all pointer
462
+ * gestures. No DOM children for clips/ticks/etc — every pixel is
463
+ * painted via 2D canvas, so even hundreds of clips render in <2ms.
464
+ */
465
+ declare class Timeline {
466
+ private root;
467
+ private opts;
468
+ private canvas;
469
+ private ctx;
470
+ private thumbs;
471
+ private hiddenHost;
472
+ private toolbarEl;
473
+ /**
474
+ * Public flex slot at the left of the top toolbar. `null` when
475
+ * `toolbar` is disabled. Hosts append their own elements (e.g. a
476
+ * size/aspect dropdown). React/Vue wrappers portal children here.
477
+ */
478
+ readonly toolbarLeft: HTMLDivElement | null;
479
+ /** Right-side counterpart — conventionally used for export/save. */
480
+ readonly toolbarRight: HTMLDivElement | null;
481
+ private project;
482
+ private pxPerSec;
483
+ private timeMs;
484
+ private selectedClipId;
485
+ private snapEnabled;
486
+ private showHeader;
487
+ private readOnly;
488
+ private autoFitEnabled;
489
+ private locale;
490
+ private scrollLeft;
491
+ private scrollTop;
492
+ private viewportWidth;
493
+ private viewportHeight;
494
+ /**
495
+ * `Date.now()` of the last interaction with each scrollbar (scroll
496
+ * change OR hover OR drag). Drives the macOS-style fade — bars are
497
+ * fully opaque for SCROLLBAR_FADE_HOLD_MS after activity, then ease
498
+ * out over the next SCROLLBAR_FADE_OUT_MS.
499
+ */
500
+ private lastScrollInteractY;
501
+ private lastScrollInteractX;
502
+ private hoverScrollbarY;
503
+ private hoverScrollbarX;
504
+ /** When set, pointer is dragging a scrollbar thumb. */
505
+ private scrollbarDrag;
506
+ private hoveredClipId;
507
+ private hoveredTrackIndex;
508
+ private hoverCursor;
509
+ private dropTargetTrackIndex;
510
+ private snapX;
511
+ private drag;
512
+ /**
513
+ * In-flight ghost of the clip being dragged. Decoupled from the
514
+ * project data so the data stays clean and undo-able only commits
515
+ * on release. Has both the proposed `start` (X) and `trackIndex`
516
+ * (Y), so the rendered ghost follows the cursor across tracks.
517
+ */
518
+ private dragGhost;
519
+ /**
520
+ * Most recent local pointer coords during a move drag — used by the
521
+ * edge-autoscroll loop to re-run drop-target resolution between
522
+ * pointermove events while scrollTop ticks under a stationary cursor.
523
+ */
524
+ private lastDragPointerX;
525
+ private lastDragPointerY;
526
+ private dragScrollRafPending;
527
+ private rafPending;
528
+ private hasAutoFitted;
529
+ private resizeObs;
530
+ private destroyed;
531
+ static create(opts: TimelineOptions): Timeline;
532
+ constructor(opts: TimelineOptions);
533
+ /**
534
+ * Sync the project data. Does NOT reset the auto-fit latch — that's
535
+ * what caused the editor-side zoom feedback loop: every Editor
536
+ * mutation called `ui.render() → timeline.setProject()` which used
537
+ * to reset auto-fit, refit on the next frame, emit a new scale,
538
+ * which re-rendered… and round we went. Callers that genuinely
539
+ * want a re-fit (e.g. when the host swaps to a brand-new project)
540
+ * should call `refit()` explicitly.
541
+ */
542
+ setProject(p: Project): void;
543
+ /** Force a re-fit on the next render. */
544
+ refit(): void;
545
+ getProject(): Project;
546
+ setTime(timeMs: Ms): void;
547
+ getTime(): Ms;
548
+ setScale(pxPerSec: number): void;
549
+ getScale(): number;
550
+ setSelection(id: string | null): void;
551
+ getSelection(): string | null;
552
+ setSnap(snap: boolean): void;
553
+ getSnap(): boolean;
554
+ setLocale(locale: Partial<Locale>): void;
555
+ /** Fit the project's full duration into the current viewport width. */
556
+ fitToWindow(): void;
557
+ /**
558
+ * Test/debug introspection — pixel coordinates of every visible clip,
559
+ * the playhead, and the headers. Because clips are canvas-painted
560
+ * there are no DOM nodes to query in e2e; tests use this instead.
561
+ * Exposed publicly so React/Vue wrappers can forward it to a ref.
562
+ */
563
+ getDebugInfo(): {
564
+ pxPerSec: number;
565
+ scrollLeft: number;
566
+ viewportWidth: number;
567
+ viewportHeight: number;
568
+ playheadX: number;
569
+ clips: Array<{
570
+ id: string;
571
+ trackIndex: number;
572
+ x: number;
573
+ width: number;
574
+ y: number;
575
+ height: number;
576
+ }>;
577
+ };
578
+ destroy(): void;
579
+ private resizeCanvas;
580
+ private computeFitScale;
581
+ private maxScrollLeft;
582
+ private maxScrollTop;
583
+ private clampScroll;
584
+ /**
585
+ * Scrollbar opacity = full for SCROLLBAR_FADE_HOLD_MS after last
586
+ * interaction, then linearly fades to 0 over SCROLLBAR_FADE_OUT_MS.
587
+ * Hovering or actively dragging the bar pins opacity at 1. Returns
588
+ * 0 if the bar isn't needed (content fits).
589
+ */
590
+ private scrollbarOpacity;
591
+ /** Mark a scrollbar axis as just-touched so its fade timer restarts. */
592
+ private touchScrollbar;
593
+ private scheduleRender;
594
+ /**
595
+ * Keep the raf loop alive while a scrollbar is still in its HOLD or
596
+ * fade-out window. Without this, opacity is sampled once and the
597
+ * bar would freeze at whatever value it had at the last input event
598
+ * instead of smoothly fading out. Skipped when a bar is pinned
599
+ * (hover or active drag) since opacity is constant there.
600
+ */
601
+ private maybeContinueFade;
602
+ private maybeAutoFit;
603
+ private buildDrawState;
604
+ private readStyle;
605
+ private attachPointer;
606
+ private onPointerDown;
607
+ private onPointerMove;
608
+ /**
609
+ * Update dragGhost + dropTargetTrackIndex for the in-flight move
610
+ * drag, given the current viewport pointer position. Pulled out of
611
+ * onPointerMove so the edge-autoscroll loop can re-run it on each
612
+ * tick — autoscroll moves scrollTop under a stationary cursor, and
613
+ * the ghost must follow the new row under that cursor.
614
+ */
615
+ private processMoveDrag;
616
+ /**
617
+ * Px-per-frame scroll speed when the pointer is in a vertical edge
618
+ * zone of the track region. Returns 0 outside the zone. Speed ramps
619
+ * linearly from 0 at the zone's inner edge to ~16 px/frame at the
620
+ * outer edge, so brushing the edge gives a gentle nudge and parking
621
+ * deep at it gives a brisk auto-scroll.
622
+ */
623
+ private dragScrollSpeedY;
624
+ /**
625
+ * Drive vertical auto-scroll while the user holds a clip near the
626
+ * top/bottom edge of the track area. Self-stopping — exits the loop
627
+ * once the pointer leaves the zone, the drag ends, or scroll bottoms
628
+ * out at the clamp.
629
+ */
630
+ private maybeStartDragAutoScroll;
631
+ private onPointerUp;
632
+ private attachWheel;
633
+ private attachResize;
634
+ private localCoords;
635
+ private hitTarget;
636
+ private trackIndexAtY;
637
+ private applySnap;
638
+ }
639
+
640
+ export { type Clip, Editor, type EditorApi, type EditorEventMap, type EditorEventName, type EditorOptions, HEADER_WIDTH, type Locale, type MediaSource, type Ms, type Project, RULER_HEIGHT, TRACK_HEIGHT, type Theme, Timeline, type TimelineOptions, type Track, createEmptyProject, createId, formatLabel, localeEn, localeZh, mergeLocale, normalizeProject };