Skip to content

feat(studio): timeline inline expansion + __clipTree runtime primitive#1468

Open
miguel-heygen wants to merge 1 commit into
mainfrom
feat/timeline-inline-expansion
Open

feat(studio): timeline inline expansion + __clipTree runtime primitive#1468
miguel-heygen wants to merge 1 commit into
mainfrom
feat/timeline-inline-expansion

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

Timeline inline expansion + expanded-clip editing

When you select a child element inside a sub-composition, the timeline replaces the parent scene clip with that depth's siblings as individual, editable clips. Deselect (or click the empty preview area) collapses back. Built on a new window.__clipTree runtime primitive.

Runtime — packages/core/src/runtime/clipTree.ts

  • New window.__clipTree: hierarchical ClipNode tree (id/parentId/children) built from all [data-start] elements, plus updateTiming() which patches data-start/data-duration on the DOM and invalidates GSAP timelines for instant feedback (no iframe reload).
  • Built before the timeline postMessage so Studio sees it on first sync; rebuilt only when the [data-start] element count changes (sub-comps loading), not every transport tick.
  • RuntimeTimelineLike gains invalidate?() so the call is typed.
  • Intentionally minimal: the node carries only what consumers read. (An earlier draft had getNode/getAbsoluteTime/onChange/postMessage/label/kind/etc. with zero callers — stripped.)

Studio

  • useExpandedTimelineElements — pure useMemo deriving the expanded view from selectedElementId + clipParentMap. No useEffect, no store writes. Each expanded child is rebased onto its own sub-comp host (expandedParentStart + sourceFile), correct at any nesting depth.
  • findMatchingTimelineElementId — returns a sourceFile#id qualified id for sub-comp children that have no top-level timeline element, so the expansion hook can resolve them.
  • NLELayout — move/resize/delete/split on expanded clips rebase absolute→local time via expandedParentStart, remap id ← domId, and route through the existing source-patch persistence (sourceFile-aware). Move/resize also patch the DOM via __clipTree.updateTiming for instant feedback. Clicking the empty preview area deselects.
  • gsapTargetCache — O(1) cached Set+WeakSet GSAP-target lookup replacing the O(n²) inline scan.

GSAP / keyframe correctness (traced per path)

All edit paths resolve the target file from the element/selection sourceFile and scan all window.__timelines (which include inlined sub-comp timelines):

  • Split GSAP retarget → useRazorSplit uses element.sourceFile. ✅
  • Keyframe delete/move/ease/toggle → useGsapScriptCommits / StudioPreviewArea use selection.sourceFile. ✅
  • Keyframe cache → keyed sourceFile#id, scans all timelines. ✅
  • GSAP-targeted drag guard → reads all timelines, covers expanded clips. ✅
  • Multi-level nesting (sub-comp inside sub-comp) — was a corruption bug (edits keyed off the top-level host, wrong file + offset); now each child rebases onto its real immediate sub-comp host. Unit-tested (useExpandedTimelineElements.test.ts, 1-level + 2-level cases).

Verification

  • Delete on expanded clips — verified e2e (agent-browser): removed from the sub-composition HTML, index.html and other scenes untouched.
  • Expansion / sub-comp expansion / move / resize / deselect — confirmed manually across hero, feature-grid, stats-panel, and inline scenes.
  • Build, typecheck, lint, oxfmt, fallow gate (zero introduced findings) all pass. Unit coverage for findMatchingTimelineElementId and buildExpandedElements.

Manual-confirm checklist before relying on these (drag/seek paths the automated harness can't drive)

  • Split an expanded clip with the razor tool → two clips, correct local times, both present in the sub-comp HTML, GSAP animation not corrupted.
  • Delete keyframe on an expanded clip → removed from the child's GSAP animation in the sub-comp script only.
  • Move/ease-change/toggle keyframe on an expanded clip → mutates the correct child animation.
  • Move + resize an expanded clip → data-start/data-duration persist to the sub-comp HTML at correct local time.

Known limitation

  • The s-key split shortcut looks up the selection in raw (non-expanded) elements, so it no-ops on an expanded clip. The razor-tool click path covers split; left as follow-up.

Plan: docs/plans/2026-06-15-003-feat-expanded-clip-editing-plan.md

@miguel-heygen miguel-heygen force-pushed the feat/timeline-inline-expansion branch 3 times, most recently from 4a7cda7 to 071ddcd Compare June 15, 2026 21:14
When a child element inside a sub-composition is selected, the timeline
replaces the parent scene clip with the deepest-level siblings. Deselect
or selecting outside collapses back. Expanded clips are fully editable —
move, resize, delete, and split — addressed by their real DOM id with
timeline time rebased onto the sub-comp they live in.

Runtime:
- New window.__clipTree API: a read-only hierarchical ClipNode tree
  (id/parentId/children + backing element) so Studio can derive
  parent/child relationships for inline expansion.

Studio:
- useExpandedTimelineElements derives the expanded view from
  selectedElementId + clipParentMap (pure useMemo, no useEffect).
  Each child rebases onto its immediate sub-comp host (start +
  sourceFile), so multi-level nesting targets the right file.
- NLELayout routes expanded-clip edits through the same handlers
  top-level clips use, in local coordinates — edits save to the
  sub-comp source and reflect via reloadPreview (no separate DOM-patch
  path). This is the canonical update; there is no reactive observer.
- findMatchingTimelineElementId resolves sub-comp children with no
  top-level element to `sourceFile#id`.
- Razor tool enabled by default; studio_razor_split analytics event
  fired on single and split-all.
- O(n²) isElementGsapTargeted extracted to gsapTargetCache.ts with a
  cached Set+WeakSet O(1) lookup.
@miguel-heygen miguel-heygen force-pushed the feat/timeline-inline-expansion branch from 071ddcd to 8a3c90e Compare June 15, 2026 21:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant