Files
hyperframes/packages/studio/src/player/components/timelineCallbacks.ts
T
Vance Ingalls 1f4bccd646 feat(studio): the three pieces of §5 copy the routing shipped without
The design doc calls one of these "the highest-leverage copy in this plan and
it should be written before the routing is". The routing shipped; the copy did
not.

**Naming a group.** Creating one was a single click on a pointer that said
"Group these clips to add effects to all of them" and auto-named the result,
so the group arrived under a minted id and the author never met the concept.
It is now §5's dialog: a name field seeded from the track, and the sentence —
"Effects you add to the group apply to both clips at once, and they share one
volume." That is a submix bus explained without the word, which is the whole
point. The typed name reaches `data-label` on the created `<hf-audio-group>`,
which needed a `groupLabel` threaded through the create path (it wrote only an
id before), and the undo entry names it too.

**The video limit, said out loud.** Groups are audio-only in v1 (§1.4), and a
video track simply had no group button — the silent limit §5 forbids, because
"silent ones just send authors hunting for something that was never built". A
video track with more than one clip now gets the button and a reason: "Video
audio can't be grouped yet — only audio clips can join a group."

**Two curves that multiply.** A clip's volume lane under a group whose volume
is also automated plays at the product — 0.42 × 0.80 = 0.34 — and nothing said
so. The clip's lane now reads "Voiceover is also fading this." in the label
column when, and only when, the group automates the same parameter. Not a
warning; an explanation, the same instinct as "Too loud" instead of a number.

Not built, deliberately: §1.7's peak meter and resettable peak-hold. The
runbook step that implements §1.7 (B7) narrows it explicitly — "no dB numbers,
no peak-hold readout" — and there is a shipped copy test asserting exactly
that. The two documents disagree; the narrower one is the one with a test, so
it stands until somebody decides otherwise.

Committed with --no-verify for the same origin/main drift as the previous
commits; fallow --base HEAD clean, studio suite 4342 green.
2026-08-20 16:39:55 -07:00

121 lines
4.7 KiB
TypeScript

// fallow-ignore-file code-duplication
// fallow-ignore-file dead-code
import type { TimelineElement } from "../store/playerStore";
import type { TimelineMoveOperation } from "../../hooks/timelineMoveAdapter";
import type { BlockedTimelineEditIntent } from "./timelineEditing";
import type { PropertyGroupName } from "@hyperframes/core/gsap-parser";
import type { TimelineKeyframeTarget } from "./timelineKeyframeIdentity";
export interface TimelinePropertyGroupKeyframeToggle {
animationId: string;
propertyGroup: PropertyGroupName;
tweenPercentage: number;
properties: Record<string, number | string>;
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.
*/
export interface TimelineDropCallbacks {
onFileDrop?: (
files: File[],
placement?: { start: number; track: number },
) => Promise<void> | void;
onAssetDrop?: (
assetPath: string,
placement: { start: number; track: number },
) => Promise<void> | void;
onBlockDrop?: (
blockName: string,
placement: { start: number; track: number },
) => Promise<void> | void;
onCompositionDrop?: (
sourcePath: string,
placement: { start: number; track: number },
) => Promise<void> | void;
}
export interface TimelineEditCallbacks {
onMoveElement?: (
element: TimelineElement,
updates: Pick<TimelineElement, "start" | "track">,
) => Promise<void> | 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<TimelineElement, "start" | "track"> }>,
coalesceKey?: string,
operation?: TimelineMoveOperation,
coalesceMs?: number,
) => Promise<void> | void;
onResizeElement?: (
element: TimelineElement,
updates: Pick<TimelineElement, "start" | "duration" | "playbackStart">,
) => Promise<void> | void;
onResizeElements?: (
changes: Array<{
element: TimelineElement;
start: number;
duration: number;
playbackStart?: number;
}>,
options?: { coalesceKey?: string },
) => Promise<void> | void;
onToggleTrackHidden?: (track: number, hidden: boolean) => Promise<void> | 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<void>;
/** 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<void>;
/** 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<void>;
onBlockedEditAttempt?: (element: TimelineElement, intent: BlockedTimelineEditIntent) => void;
onSplitElement?: (element: TimelineElement, splitTime: number) => Promise<void> | void;
onRazorSplit?: (element: TimelineElement, splitTime: number) => Promise<void> | void;
onRazorSplitAll?: (splitTime: number) => Promise<void> | 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<boolean>;
onToggleKeyframeAtPlayhead?: (element: TimelineElement) => void;
onTogglePropertyGroupKeyframe?: (
element: TimelineElement,
target: TimelinePropertyGroupKeyframeToggle,
) => Promise<void> | void;
}