import * as react_jsx_runtime from 'react/jsx-runtime'; import * as react from 'react'; import { ReactNode, RefObject } from 'react'; import { RuntimeTimelineClipIdentity } from '@hyperframes/core'; import { GsapAnimation, PropertyGroupName } from '@hyperframes/core/gsap-parser'; import * as zustand from 'zustand'; import { MusicBeatAnalysis } from '@hyperframes/core/beats'; import { GsapAnimation as GsapAnimation$1, ArcPathSegment } from '@hyperframes/parsers/gsap-parser'; interface PlayerProps { projectId?: string; directUrl?: string; onLoad: () => void; onCompositionLoadingChange?: (loading: boolean) => void; portrait?: boolean; style?: React.CSSProperties; suppressLoadingOverlay?: boolean; } /** * Renders a composition preview using the web component. * * The web component handles iframe scaling, dimension detection, and * ResizeObserver internally. This wrapper bridges its inner iframe to the * forwarded ref so useTimelinePlayer can access it for clip manifest parsing, * timeline probing, and DOM inspection. */ declare const Player: react.ForwardRefExoticComponent>; interface PlayerControlsProps { onTogglePlay: () => void; onSeek: (time: number) => void; disabled?: boolean; isFullscreen?: boolean; onToggleFullscreen?: () => void; } declare const PlayerControls: react.NamedExoticComponent; interface UserBeat { time: number; strength: number; } interface BeatEditState { /** Music src these edits apply to; edits reset when the src changes. */ src: string; /** Beats the user added (audio-file coords). */ added: UserBeat[]; /** Audio-file times of detected beats the user removed. */ removed: number[]; } /** * Shared type definitions for the timeline playback subsystem. * Kept in a separate module so adapter, DOM, and hook modules can all import * from here without creating circular dependencies. */ interface ClipManifestClip extends RuntimeTimelineClipIdentity { zIndex?: number; stackingContextId?: string | null; compositionAncestors?: string[]; playbackStart?: number; playbackRate?: number; } type TimelineTimeDisplayMode = "time" | "frame"; interface AnimationKeyframeTarget { animationId: string; tweenPercentage: number; } /** Minimal keyframe cache types — mirrors GsapKeyframesData without pulling in Node-only gsap-parser. */ interface KeyframeCacheEntry { format: string; keyframes: Array<{ percentage: number; /** Original tween-relative percentage (server mutations need this, not the clip-relative `percentage`). */ tweenPercentage?: number; /** Which property group the source tween belongs to (position, scale, rotation, visual, etc.). */ propertyGroup?: string; /** Source tween id — lets the inline clip-row ease button target a specific segment. */ animationId?: string; properties: Record; ease?: string; /** Source animation/keyframe targets that collide at this clip percentage. */ collidingAnimationTargets?: AnimationKeyframeTarget[]; }>; ease?: string; easeEach?: string; } interface FocusedEaseSegment { animationId: string; collidingAnimationTargets?: AnimationKeyframeTarget[]; tweenPercentage: number; elementId: string; projectId: string | null; sessionEpoch: number; nonce: number; } type FocusedEaseSegmentTarget = Omit; /** * A request to reveal one automated parameter in the audio FX rack. * * `elementKey` is the timeline element whose rack it belongs to — a group's own * id for a group lane, the clip's key otherwise — so the panel can refuse a * request aimed at something it is not showing. */ interface RevealedAudioFxTarget { elementKey: string; /** The lane's own `fx..` / `volume` target. */ automationTarget: string; projectId: string | null; sessionEpoch: number; nonce: number; } type RevealedAudioFxTargetRequest = Omit; interface KeyframeSlice { /** Selected collapsed (`element:pct`) or expanded (`element:group:animation:clipPct`) diamonds. */ selectedKeyframes: Set; toggleSelectedKeyframe: (key: string) => void; clearSelectedKeyframes: () => void; /** Clips whose keyframe property lanes are expanded in the timeline. */ expandedClipIds: Set; toggleClipExpanded: (id: string) => void; setClipExpanded: (id: string, expanded: boolean) => void; /** Union-expand clips (keyframed clips are expanded by default on load). */ expandClips: (ids: readonly string[]) => void; /** * Groups whose member rows the caret has HIDDEN (structural, not lanes). * * Inverted deliberately. As an expanded-set, "not in the set" could not tell * never-touched from deliberately-collapsed, so every group defaulted to * collapsed — and since nothing seeds the set on create, grouping three * tracks made all three vanish behind a header the user had not yet learned * to open. Groups are expanded until someone closes one. */ collapsedGroupIds: Set; toggleGroupExpanded: (id: string) => void; /** Rows (clip id or group id) whose automation-lane rows the `∿` button opened. */ expandedLaneOwnerIds: Set; toggleLaneOwnerExpanded: (id: string) => void; /** * Project/session/element-scoped request. Its nonce is monotonic across store * resets so a stale consumer can never collide with a later request. */ focusedEaseSegment: FocusedEaseSegment | null; focusedEaseRequestNonce: number; setFocusedEaseSegment: (target: FocusedEaseSegmentTarget) => void; clearFocusedEaseSegment: (nonce: number) => void; /** * "Show me this automated parameter in the rack" — raised by clicking an * automation lane's label in the timeline, consumed by the property panel. * * Session-stamped and nonce-guarded exactly like `focusedEaseSegment`: a * request outlives the click, so one made against a different project or * before a reload must not reopen a rack on whatever is mounted later. */ revealedAudioFxTarget: RevealedAudioFxTarget | null; revealedAudioFxNonce: number; setRevealedAudioFxTarget: (target: RevealedAudioFxTargetRequest) => void; clearRevealedAudioFxTarget: (nonce: number) => void; /** Keyframe data per element id, populated from parsed GSAP animations. */ keyframeCache: Map; /** Unmerged source tweens per element; expanded property lanes read this, never keyframeCache. */ gsapAnimations: Map; setGsapAnimations: (elementId: string, animations: GsapAnimation[] | undefined) => void; setKeyframeCache: (elementId: string, data: KeyframeCacheEntry | undefined) => void; } /** * The active selection box on one automation lane. * * A store slice, not lane-local state, for the same reason keyframe selection * is one: Delete/copy/paste handlers and the shape menu live outside the lane * component and need to read it. Ephemeral by construction — nothing * serializes store state, and the selection must never survive into a render. */ interface AutomationSelection { /** TimelineElement key (key ?? id) of the clip that owns the lane. */ elementKey: string; /** Lane target: "volume" or "fx..". */ target: string; /** Clip-local seconds; always t0 <= t1 (ordered on write). */ t0: number; t1: number; /** * The box's value bounds, in the parameter's own units; always v0 <= v1. * * What makes the selection a box rather than a time span: a point is caught * only if it falls inside both axes. Span operations — copy, paste, shape * insert, simplify — still read t0/t1 alone, because they act on the envelope * over a stretch of time rather than on a set of breakpoints. */ v0: number; v1: number; } interface AutomationSelectionSlice { automationSelection: AutomationSelection | null; setAutomationSelection: (sel: AutomationSelection) => void; clearAutomationSelection: () => void; } /** * The two editing-mode toggles the timeline toolbar owns. * * A store slice rather than toolbar-local state because both are read far from * where they are set: the motion-path arm is consumed by the canvas click * handler and published by MotionPathOverlay, and the auto-keyframe flag * decides what a drag/resize/rotate does inside the edit commit path. * * `motionPathArmed` is also cleared by playerStore on selection change — arming * survives neither a new element nor a project switch, so those resets stay * with the state they clear alongside. */ interface EditingModeSlice { /** Motion-path "set destination" mode. Armed from the preview toolbar * (replaces the old double-click-on-canvas UX); while armed, one canvas * click places the new path's destination. */ motionPathArmed: boolean; setMotionPathArmed: (armed: boolean) => void; /** Published by MotionPathOverlay so the toolbar shows the button only when * the selected element can actually take a path. */ motionPathCreateAvailable: boolean; setMotionPathCreateAvailable: (available: boolean) => void; /** Global toggle for the "Add keyframe" diamond in the timeline toolbar * (#1808). When false, a manual drag/resize/rotate edit on an element that * already has a live tween shifts every keyframe by the edit's delta * (preserving the animation's shape) instead of inserting/updating a * keyframe at the playhead. */ autoKeyframeEnabled: boolean; setAutoKeyframeEnabled: (enabled: boolean) => void; } interface TimelineFocusRequest { id: string; projectId: string | null; sessionEpoch: number; nonce: number; } type ThumbnailMode = "adaptive" | "hidden"; interface ThumbnailSlice { thumbnailMode: ThumbnailMode; setThumbnailMode: (mode: ThumbnailMode) => void; } type TimeListener = (time: number) => void; declare const liveTime: { notify: (time: number) => void; subscribe: (listener: TimeListener) => () => boolean; }; /** * One row in the timeline: a clip as Studio needs it, translated from the * runtime's manifest at createTimelineElementFromManifestClip. * * Split out of playerStore, which had reached the 600-line studio ceiling and * could not carry another field. Re-exported from there, so every existing * importer is unaffected. */ interface TimelineElement { id: string; label?: string; key?: string; kind?: ClipManifestClip["kind"]; tag: string; start: number; duration: number; track: number; /** * The data-track-index as written in the source file. Set at the manifest * translation boundary (createTimelineElementFromManifestClip) from the * runtime clip's verbatim track, and preserved through display-lane remaps * (normalizeToZones packs sparse authored tracks onto contiguous display * lanes; expanded sub-comp children get synthetic display rows). Lane edits * must persist THIS space — writing a display-lane number into a sparse file * re-targets the wrong track. For an expanded child the value is in its OWN * source file's coordinate space, not the host timeline's. */ authoredTrack?: number; /** Resolved z-index for stacking-aware timeline ordering. */ zIndex?: number; /** True when the effective z-index was authored inline or through CSS, not auto. */ hasExplicitZIndex?: boolean; /** Canonical CSS stacking context this element's z-index participates in. */ stackingContextId?: string | null; /** Nearest parent composition context, matching RuntimeTimelineClip. */ parentCompositionId?: string | null; /** Composition ancestry from root to nearest parent, matching RuntimeTimelineClip. */ compositionAncestors?: string[]; domId?: string; /** Stable `data-hf-id` attribute value — used as primary patch target when present */ hfId?: string; /** Best-effort selector used when patching source HTML back from timeline edits */ selector?: string; /** Zero-based occurrence index for non-unique selectors */ selectorIndex?: number; /** Source composition file that owns this element, when known */ sourceFile?: string; src?: string; playbackStart?: number; playbackStartAttr?: "media-start" | "playback-start"; playbackRate?: number; sourceDuration?: number; volume?: number; /** Verbatim `data-fx-chain` / `data-automation`; see automationLaneData. */ fxChain?: string; automation?: string; /** Path from data-composition-src — identifies sub-composition elements */ compositionSrc?: string; /** Whether this row came from authored clip timing or Studio's full-duration layer fallback. */ timingSource?: "authored" | "implicit"; /** Set by data-timeline-locked on the host element — disables move and trim in Studio. */ timelineLocked?: boolean; /** Set by data-hidden on the host element — hides the clip in preview and render. */ hidden?: boolean; /** Value of data-timeline-role attribute — used to identify music vs. voiceover. */ timelineRole?: string; /** Verbatim `data-audio-group` — the id of the `` this clip belongs to, when any. */ audioGroup?: string; /** The owning group's `data-label` (falls back to its id) — resolved once per parse. */ audioGroupLabel?: string; /** The owning group's `data-volume` (defaults to 1) — resolved once per parse. */ audioGroupVolume?: number; /** The owning group's `data-hidden` (defaults to false) — resolved once per parse. */ audioGroupHidden?: boolean; /** The owning group's serialized `data-fx-chain`, when set — resolved once per parse. */ audioGroupFxChain?: string; audioGroupAutomation?: string; /** * Set by useExpandedTimelineElements on an inline-expanded sub-composition * child: the absolute master-timeline start of the sub-comp host the child * lives in. Presence marks the element as expanded; edits subtract it to get * the child's local (sourceFile-relative) time. Works at any nesting depth. */ expandedParentStart?: number; expandedHostKey?: string; } /** * The fields `updateElement` may write. * * Deliberately a narrow allow-list rather than `Partial`: most * of an element is derived from the document at parse time, and letting a * caller poke those would put the store out of step with the file it mirrors. * * The `audioGroup*` entries are the GROUP's state, mirrored onto every member — * a group row derives its label, fader, mute and chain from these, so a group * write has to be able to land here or the header goes on rendering whatever it * parsed at load. */ type TimelineElementPatch = Partial>; /** * The `data-*` state an expanded sub-composition child needs but cannot reach. * * A child row is synthesized from a manifest clip with no element to read, and * for a real sub-composition it has no flat store twin either: such clips are * dropped before the flat store is built. Read off the live preview instead * (`collectSubCompositionHostState`) and carried on the store by dom id. * * Without it the eye reported every hidden child visible, so clicking it wrote * `data-hidden` a second time instead of removing it, and the element could * never be shown again, not even after a reload, since the attribute is in the * source. */ interface SubCompositionHostState { hidden?: boolean; timelineLocked?: boolean; timelineRole?: string; fxChain?: string; automation?: string; } type ZoomMode = "fit" | "manual"; type TimelineTool = "select" | "razor"; interface SelectElementOptions { preserveSet?: boolean; } interface PlayerState extends KeyframeSlice, AutomationSelectionSlice, ThumbnailSlice, EditingModeSlice { isPlaying: boolean; currentTime: number; duration: number; timelineReady: boolean; /** Increments exactly once when the Studio switches to a different project. */ timelineSessionEpoch: number; /** Project owning the current timeline session; null outside a project-scoped reset. */ timelineProjectId: string | null; /** True while a beat dot is being dragged — hides the playhead guideline. */ beatDragging: boolean; elements: TimelineElement[]; selectedElementId: string | null; playbackRate: number; audioMuted: boolean; audioVolume: number; loopEnabled: boolean; /** Timeline zoom: 'fit' auto-scales to viewport, 'manual' uses manualZoomPercent */ zoomMode: ZoomMode; /** Timeline zoom percent relative to the fit width when in manual mode */ manualZoomPercent: number; /** * Bumped on every live z-index edit (handleDomZIndexReorderCommit apply AND * rollback). Flashless z commits (skipReload) never reload the iframe or * bump refreshKey, so DOM-derived views (the Layers panel's z-sorted tree) * subscribe to this to re-read the live DOM while playback is paused. */ zEditVersion: number; /** Work-area in-point (seconds). When set, loop starts here and A jumps here. */ inPoint: number | null; /** Work-area out-point (seconds). When set, loop ends here and E jumps here. */ outPoint: number | null; activeTool: TimelineTool; setActiveTool: (tool: TimelineTool) => void; /** Tween-relative percentage of the last-clicked keyframe diamond. Operations * (drag, resize, rotate) target this instead of recomputing from playhead. */ activeKeyframePct: number | null; setActiveKeyframePct: (pct: number | null) => void; /** Multi-select: additional selected elements beyond selectedElementId. */ selectedElementIds: Set; clearSelectedElementIds: () => void; /** Replace the whole multi-selection at once (marquee live updates). */ setSelectedElementIds: (ids: Set) => void; /** Timeline magnet toggle — when false, clip drags/trims/drops never snap. */ timelineSnapEnabled: boolean; setTimelineSnapEnabled: (enabled: boolean) => void; /** Transport + ruler readout: timecode ("time") or frame number ("frame"). */ timeDisplayMode: TimelineTimeDisplayMode; setTimeDisplayMode: (mode: TimelineTimeDisplayMode) => void; /** * Pin the timeline zoom to its current visual scale before a duration-changing * edit, so a subsequent duration change (which recomputes fit-pps) stops * rescaling every clip. No-op once already pinned (mode is "manual"). */ pinTimelineZoom: (currentPixelsPerSecond: number, fitPixelsPerSecond: number) => void; /** The timeline's live pixels-per-second + fit basis, published by . */ timelinePps: number; timelineFitPps: number; setTimelineScale: (pps: number, fitPps: number) => void; setSelection: (ids: Iterable, anchor?: string | null) => void; addSelectedElementId: (id: string) => void; toggleSelectedElementId: (id: string) => void; clearSelection: () => void; setIsPlaying: (playing: boolean) => void; setCurrentTime: (time: number) => void; setDuration: (duration: number) => void; setPlaybackRate: (rate: number) => void; setAudioMuted: (muted: boolean) => void; setAudioVolume: (volume: number) => void; setLoopEnabled: (enabled: boolean) => void; setTimelineReady: (ready: boolean) => void; setBeatDragging: (dragging: boolean) => void; setElements: (elements: TimelineElement[]) => void; setSelectedElementId: (id: string | null, options?: SelectElementOptions) => void; /** Move the selection anchor within an active multi-selection without collapsing it. */ setSelectionAnchor: (id: string | null) => void; updateElement: (elementId: string, updates: TimelineElementPatch) => void; setZoomMode: (mode: ZoomMode) => void; setManualZoomPercent: (percent: number) => void; bumpZEditVersion: () => void; setInPoint: (time: number | null) => void; setOutPoint: (time: number | null) => void; /** Owns the hard project boundary; repeated calls for one project are no-ops. */ beginTimelineSession: (projectId: string) => void; /** Clears project data without creating a new hard-project session. */ reset: () => void; /** * Request a seek from outside the player loop (e.g. Layers panel). * useTimelinePlayer subscribes and calls adapter.seek() + liveTime.notify(). */ requestedSeekTime: number | null; requestSeek: (time: number) => void; clearSeekRequest: () => void; /** * Request the transport start or stop from outside the player loop. * * The FX rack auditions a preset by writing it to the running graph, which is * silent while the transport is paused — so hovering one has to start * playback, and leaving has to put the playhead back where it was. Hovering is * not an edit and must not cost the author their place. * * A nonce rather than a bare boolean: two hovers in a row both want play, and * without it the second request is indistinguishable from the first having * already been served. */ playbackRequest: { playing: boolean; returnTo: number | null; nonce: number; } | null; requestPlayback: (playing: boolean, returnTo?: number | null) => void; clearPlaybackRequest: () => void; /** * Request the timeline to scroll a clip into view (e.g. clicking an * already-added asset card in the sidebar). Consumed and cleared by * useTimelineRevealClip. The nonce makes repeat requests for the same * clip observable so a second click re-reveals after the user scrolls away. */ clipRevealRequest: { elementId: string; nonce: number; } | null; requestClipReveal: (elementId: string) => void; clearClipRevealRequest: () => void; timelineFocus: TimelineFocusRequest | null; timelineFocusNonce: number; requestTimelineFocus: (id: string) => void; clearTimelineFocus: (nonce: number) => void; lintFindingsByElement: Map; setLintFindingsByElement: (map: Map) => void; beatAnalysis: MusicBeatAnalysis | null; setBeatAnalysis: (analysis: MusicBeatAnalysis | null) => void; /** User edits (add/move/delete) layered over the detected beat grid. */ beatEdits: BeatEditState | null; setBeatEdits: (edits: BeatEditState | null) => void; /** Undo/redo stacks for beat edits (in-memory, session-only). */ beatUndo: BeatHistoryEntry[]; beatRedo: BeatHistoryEntry[]; commitBeatEdits: (next: BeatEditState | null, label: string) => void; undoBeatEdits: () => string | null; redoBeatEdits: () => string | null; resetBeatHistory: () => void; beatPersist: (() => void) | null; setBeatPersist: (fn: (() => void) | null) => void; clipManifest: ClipManifestClip[] | null; setClipManifest: (clips: ClipManifestClip[] | null) => void; clipParentMap: Map; setClipParentMap: (map: Map) => void; /** * Sub-composition DOM descendants (groups + their children) that have no * `data-start`, so they're absent from the clip manifest/tree. Collected * studio-side from the live preview so the timeline can expand a sub-comp row * to show its DOM-only children. Keeps the manifest lean (timed clips only). */ domClipChildren: DomClipChild[]; setDomClipChildren: (children: DomClipChild[]) => void; /** * Host-element state for every id'd element inside a sub-composition, keyed by * dom id. Collected from the live preview because it is the only place that * sees it: these elements are filtered out of `elements` before the flat store * is built, and the clip manifest carries timing, not attributes. */ subCompositionHostState: Map; setSubCompositionHostState: (state: Map) => void; } /** A sub-comp DOM-only timeline child (no data-start) and its nesting context. */ interface DomClipChild { id: string; parentId: string; /** The manifest sub-comp host clip id this descendant ultimately lives under. */ hostId: string; label: string; stackingContextId: string; /** * The child's audio-group state, read off its live element during the DOM * walk — the only place that sees it. A sub-composition can declare a group * and its members entirely within itself, and those members never reach the * flat store, so an expanded child has no twin to inherit membership from. */ audioGroup?: string; audioGroupLabel?: string; audioGroupVolume?: number; audioGroupHidden?: boolean; audioGroupFxChain?: string; /** The group element's `data-automation`, mirrored the same way. */ audioGroupAutomation?: string; } interface BeatHistoryEntry { restore: BeatEditState | null; at: number; label: string; } declare const usePlayerStore: zustand.UseBoundStore>; /** * Source Patcher — Maps visual property edits back to source HTML files. * Handles inline style updates, attribute changes, and text content. */ interface PatchOperation { type: "inline-style" | "attribute" | "text-content" | "html-attribute" | "rich-text"; property: string; value: string | null; childSelector?: string; childIndex?: number; } interface PatchTarget { id?: string | null; hfId?: string; selector?: string; selectorIndex?: number; } /** * Find which source file contains an element by its ID. */ declare function resolveSourceFile(elementId: string | null, selector: string, files: Record): string | null; /** * Apply a patch operation to an HTML source file. */ declare function applyPatch(html: string, elementId: string, op: PatchOperation): string; interface DomEditCapabilities { canSelect: boolean; canEditStyles: boolean; /** Can take a non-destructive `clip-path: inset()` crop — broader than * canEditStyles (a sub-composition host is croppable from the parent view). */ canCrop: boolean; /** Directly editable authored left/top style fields. Canvas drag uses manual edits instead. */ canMove: boolean; /** Directly editable authored width/height style fields. Canvas resize uses manual edits instead. */ canResize: boolean; canApplyManualOffset: boolean; canApplyManualSize: boolean; canApplyManualRotation: boolean; reasonIfDisabled?: string; } interface DomEditTextField { key: string; label: string; value: string; tagName: string; attributes: Array<{ name: string; value: string; }>; inlineStyles: Record; computedStyles: Record; source: "self" | "child" | "text-node"; sourceChildIndex?: number; } interface DomEditSelection extends PatchTarget { element: HTMLElement; label: string; tagName: string; sourceFile: string; compositionPath: string; compositionSrc?: string; isCompositionHost: boolean; isInsideLockedComposition: boolean; boundingBox: { x: number; y: number; width: number; height: number; }; textContent: string | null; dataAttributes: Record; inlineStyles: Record; computedStyles: Record; textFields: DomEditTextField[]; capabilities: DomEditCapabilities; gsapAnimations?: GsapAnimation$1[]; } type BlockedTimelineEditIntent = "move" | "resize-start" | "resize-end"; type TimelineMoveOperation = "timing" | "lane-reorder" | "track-insert"; interface TimelineKeyframeTarget { percentage: number; tweenPercentage?: number; propertyGroup?: string; animationId?: string; collidingAnimationTargets?: AnimationKeyframeTarget[]; } interface TimelinePropertyGroupKeyframeToggle { animationId: string; propertyGroup: PropertyGroupName; tweenPercentage: number; properties: Record; remove: boolean; } /** * Shared callback signatures for timeline editing operations. * Used by NLELayout, Timeline, and any component that passes through * the standard set of timeline mutation handlers. */ interface TimelineDropCallbacks { onFileDrop?: (files: File[], placement?: { start: number; track: number; }) => Promise | void; onAssetDrop?: (assetPath: string, placement: { start: number; track: number; }) => Promise | void; onBlockDrop?: (blockName: string, placement: { start: number; track: number; }) => Promise | void; onCompositionDrop?: (sourcePath: string, placement: { start: number; track: number; }) => Promise | void; } interface TimelineEditCallbacks { onMoveElement?: (element: TimelineElement, updates: Pick) => Promise | void; /** Atomic multi-clip move (single undo) for main-track ripple + track-insert. * `coalesceKey` (drag-commit gesture id) merges the move history entry with a * lane change's follow-up z-reorder entry into one undo step; `coalesceMs` * widens that entry's fold window when a server round-trip separates the * gesture's records (per-gesture-unique keys keep the fold gesture-scoped). */ onMoveElements?: (edits: Array<{ element: TimelineElement; updates: Pick; }>, coalesceKey?: string, operation?: TimelineMoveOperation, coalesceMs?: number) => Promise | void; onResizeElement?: (element: TimelineElement, updates: Pick) => Promise | void; onResizeElements?: (changes: Array<{ element: TimelineElement; start: number; duration: number; playbackStart?: number; }>, options?: { coalesceKey?: string; }) => Promise | void; /** * `displayNumber` is the row the CLICKED control announced. It travels with * the click because the header and the undo-history label derive the row from * two different orderings: the header's comes from the group-aware row list * (synthetic anchor rows, members pulled contiguous), the history's from a * plain ascending sort of element-bearing keys. Once a group exists those * disagree, so the same click said "Hide track 2" and recorded "Hide track 1". * Passing the rendered number keeps one answer instead of two derivations. */ onToggleTrackHidden?: (track: number, hidden: boolean, displayNumber?: number | null) => Promise | void; /** B7's bus strip: live-write the group's own attribute while dragging. */ onSetAudioGroupAttributeLive?: (groupId: string, attr: string, value: string | null) => void; /** ...and persist one undo entry on release. */ onSetAudioGroupAttributeQuiet?: (groupId: string, attr: string, value: string | null, label: string) => Promise; /** C1's ungrouped-track FX pointer: "Group these clips" — write * `data-audio-group` on every one of them, atomically. Same shape B6's * carve auto-grouping uses. */ onGroupClips?: (clipIds: readonly string[], groupId: string, groupLabel?: string) => Promise; /** C1's single-clip FX write: addressed by the clip itself rather than the * current selection, mirroring `onSetAudioGroupAttributeLive/Quiet`. */ onSetElementAttributeLive?: (element: TimelineElement, attr: string, value: string | null) => void; onSetElementAttributeQuiet?: (element: TimelineElement, attr: string, value: string | null, label: string) => Promise; onBlockedEditAttempt?: (element: TimelineElement, intent: BlockedTimelineEditIntent) => void; onSplitElement?: (element: TimelineElement, splitTime: number) => Promise | void; onRazorSplit?: (element: TimelineElement, splitTime: number) => Promise | void; onRazorSplitAll?: (splitTime: number) => Promise | void; onDeleteKeyframe?: (elementId: string, keyframe: TimelineKeyframeTarget) => void; onDeleteAllKeyframes?: (element: TimelineElement, animationId?: string) => void; onMoveKeyframeToPlayhead?: (element: TimelineElement, keyframe: TimelineKeyframeTarget) => void; /** Drag-to-retime: `keyframe` identifies the dragged keyframe (its percentage * is clip-relative), `toClipPercentage` is the neighbour-clamped drop. */ onMoveKeyframe?: (elementId: string, keyframe: TimelineKeyframeTarget, toClipPercentage: number) => Promise; onToggleKeyframeAtPlayhead?: (element: TimelineElement) => void; onTogglePropertyGroupKeyframe?: (element: TimelineElement, target: TimelinePropertyGroupKeyframeToggle) => Promise | void; } interface TimelineTheme { shellBackground: string; shellBorder: string; rulerBorder: string; rowBackground: string; rowBorder: string; gutterBackground: string; gutterBorder: string; textPrimary: string; textSecondary: string; tickText: string; tickMajor: string; tickMinor: string; clipBackground: string; clipBackgroundActive: string; clipBorder: string; clipBorderHover: string; clipBorderActive: string; clipShadow: string; clipShadowHover: string; clipShadowActive: string; clipShadowDragging: string; handleColor: string; panelResizeSeam: string; panelResizeActive: string; clipRadius: string; } type TimelineEditOverrides = Pick; interface TimelineClipRenderContext { priority: "overscan" | "visible" | "interaction"; rich: boolean; } interface TimelineProps extends TimelineDropCallbacks, TimelineEditOverrides { /** Project-scoped reset boundary; soft source refreshes retain the same epoch. */ sessionEpoch?: number; onSeek?: (time: number) => void; onDrillDown?: (element: TimelineElement) => void; renderClipContent?: (element: TimelineElement, style: { clip: string; label: string; }, context: TimelineClipRenderContext) => ReactNode; renderClipOverlay?: (element: TimelineElement) => ReactNode; onDeleteElement?: (element: TimelineElement) => Promise | void; onSelectElement?: (element: TimelineElement | null) => void; theme?: Partial; } declare const Timeline: react.NamedExoticComponent; type ThumbnailPriority = "overscan" | "visible" | "interaction"; interface VideoThumbnailProps { videoSrc: string; label: string; labelColor: string; duration?: number; sourceStart?: number; sourceRangeDuration?: number; projectId?: string; sessionEpoch?: number; priority?: ThumbnailPriority; rich?: boolean; } /** Sparse, bounded video frames supplied by the shared thumbnail scheduler. */ declare const VideoThumbnail: react.NamedExoticComponent; interface CompositionThumbnailProps { previewUrl: string; label: string; labelColor: string; selector?: string; selectorIndex?: number; seekTime?: number; duration?: number; width?: number; height?: number; projectId?: string; sessionEpoch?: number; priority?: ThumbnailPriority; rich?: boolean; } /** Server-rendered composition poster, deduplicated and budgeted by project/session. */ declare const CompositionThumbnail: react.NamedExoticComponent; /** * Runtime iframe integration utilities. * * Handles the boundary between the studio host page and the preview iframe: * - Viewport normalisation on load * - Auto-healing missing data-composition-id attributes * - Unmuting media via postMessage * - Resolving the underlying