Skip to content

Export the timeline's sub-composition hydration helpers, or hydrate nested clips in useTimelinePlayer #6153

Open

Description

@DanAmador

What we're doing

We host Studio's components inside our own editor rather than running StudioApp, the way
hostMount.test.tsx and the TimelinePane export comments describe. That already works well:
ConnectedDomEditOverlay, PropertyPanel and useDomEditSession give us hand editing on the
picture, and useTimelinePlayer + Timeline mount cleanly in our page with no NLEProvider and
no EditorShell. Thank you — the host surface is genuinely usable.

Our documents are compositions whose every scene is a sub-composition (data-composition-src).
We want the timeline to show one scene's own elements, so a user who opens a scene can retime
its clips.

What's blocked

useTimelinePlayer fills usePlayerStore.elements from the manifest with the sub-composition
hosts only. The children never arrive, so an expanded host has nothing to show.

The runtime does publish the nesting. In the preview window:

window.__clipTree
// { roots: [ { id: "inner", parentId: null, children: [
//     { id: "ss-hud", parentId: "inner", children: [] },
//     { id: "ss-title", parentId: "inner", children: [] },
//     { id: "ss-note-1", parentId: "inner", children: [] },
//     { id: "ss-note-2", parentId: "inner", children: [] } ] } ] }

And the store accepts the expansion — after setClipExpanded(key, true):

usePlayerStore.getState().expandedClipIds  // Set { "index.html#inner" }
usePlayerStore.getState().elements         // [ { id: "inner", kind: "composition", ... } ]  ← host only

The code that bridges those two is already in the package, in
src/player/hooks/timelineSyncHydration.ts:

  • clipTreeParentMap(win) (L66) — walks __clipTree into a child → parent map
  • collectSubCompositionDomChildren(...) (L122)
  • buildTimelineElementsFromClips(...) (L200)
  • syncManifestTimeline(...) (L222)

That file's own comment (L117-119) says it plainly: the elements "carry no data-start, so the
... DOM children + parent links is what lets the timeline expand a sub-composition." That is
exactly the behaviour we want, and it is exactly what a host cannot reach.

None of the four is exported. dist/index.d.ts and dist/player.d.ts each contain zero
occurrences of all four names, and the exports map (., ./ui, ./player,
./tailwind-preset, the stylesheets, package.json) shuts deep imports of src/.

What we tried, so you don't have to suggest it

Building the children host-side and writing them in with setElements does not hold. The
player re-syncs the store from the manifest as the preview plays, so rows added from outside are
dropped on the next tick; topping them back up fights that sync and takes the page down with
"Maximum update depth exceeded". We take the lesson to be that the store is the player's and a
host should not be writing elements into it — which is why we're asking for the hydration path
rather than a way around it.

We also cannot point a preview at the sub-composition and play it alone: in our documents a
scene's file is a <template> fragment with no <html>, <body>, <script> or runtime, so it
isn't a document. That may be particular to how we generate them, and we mention it only to show
why useCompositionStack isn't the answer for us.

The ask

Either of these unblocks us; the first is smaller.

  1. Export the four helpers from . or ./player. They are already written, already tested
    and already have the semantics we want. No behaviour change, no new surface to maintain
    beyond the names.

  2. Have useTimelinePlayer hydrate nested clips itself when a host is in expandedClipIds —
    arguably where the behaviour belongs, since expandedClipIds and setClipExpanded are
    already public and today they expand to nothing for a host.

If neither suits, a documented way for a host to contribute elements that the manifest sync will
not clobber would also do.

Environment

  • @hyperframes/studio@0.8.106, @hyperframes/core@0.8.106
  • React 19, Vite, the components hosted in our own page (no StudioApp, no EditorShell)
  • Public API only: no fork, no patched dist, no deep imports

Activity

  1. Dante-dan commented on Oct 23, 2026

    @Dante-dan
    Contributor

    I traced this against current main (2512642d09d3d5a4703fd2e6af71930a52e6c7ca). There is an important distinction between hydration and the rows the timeline displays:

    • useTimelinePlayer already calls useTimelineSyncCallbacks, whose manifest handler populates clipParentMap, domClipChildren, and subCompositionHostState. Children do not have to appear in elements for that hydration to have happened. Current handler.
    • feat(studio): the timeline shows one row per top-level element and no child rows #4132 deliberately removed the expanded child-row implementation and made the timeline top-level-only. Its description explicitly leaves opening a sub-composition as its own timeline for a separate change. That means exporting the four helpers alone would expose the data conversion, but would not restore child rows in the current Timeline.

    For your fragment-based host, I would propose an opt-in scene-scoped timeline view: keep the existing preview and manifest as the timing authority, derive a selected scene's rows from its hydrated descendants, and leave the normal Studio top-level default intact. The important acceptance cases would be changing scenes, preserving rows across subsequent manifest messages, and making a child timing edit target the scene's source rather than the parent host. This needs a supported row/edit boundary; a host-side setElements loop would keep fighting the player.

    Would maintainers prefer that host scene-view scope, or a deliberately low-level helper export for hosts supplying their own row view? Also, on a current package version, are domClipChildren and clipParentMap populated after the preview loads? That separates a remaining hydration problem from the row-model limitation. I have inspected the source paths; I have not reproduced your hosted editor.

  2. jrusso1020 commented on Oct 24, 2026

    @jrusso1020
    Collaborator

    Thanks for the detailed write-up and for listing what you already tried. Confirmed: the four helpers are exported from packages/studio/src/player/hooks/timelineSyncHydration.ts (lines 67, 123, 201, 223), but none of the package entry points re-export them, so a host can't reach them. One detail worth knowing: #4132 made the Timeline show top-level rows only, on purpose. So exporting the helpers would give you the hydrated data but would not make the stock Timeline show child rows by itself. Choosing between a low-level helper export and a supported scene-scoped timeline view is a design call for the maintainers, so I'm tagging this as a feature request. — Rames

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions