Skip to content

feat(studio): zoom the timeline to the selected clips - #5824

Merged
miguel-heygen merged 4 commits into
heygen-com:mainfrom
DanielMcSheehy:feat/timeline-zoom-to-selection
Oct 26, 2026
Merged

miguel-heygen merged 4 commits into
heygen-com:mainfrom
DanielMcSheehy:feat/timeline-zoom-to-selection

Conversation

@DanielMcSheehy

@DanielMcSheehy DanielMcSheehy commented Oct 18, 2026 •

Copy link
Copy Markdown

What

A timeline toolbar action (next to Fit) and a Z shortcut that zoom and scroll so the selected clips span the visible width, with the same 24px margin the range zoom uses. With nothing the zoom can land on, the button is disabled and its tooltip says why.

Demo

Nothing selected, the button stays disabled and the tooltip says to select a clip. Selecting the last clip, hovering the toolbar action (the tooltip shows Z), and clicking it: the eased zoom goes from Fit to that clip spanning the visible width, with its right edge on the margin. Zoom in still steps the scale. Then Z on the clip at time 0, which stays on screen without scrolling past zero.

zoom-selection-followup.mp4

Before

Fit shows the whole span. The last clip is a small block at the right.

https://github.com/user-attachments/assets/ba5415bb-69bd-47d5-b19e-71e388a1d3a8

After

Zoom to that clip. It fills the visible width, and the zoom readout leaves Fit.

https://github.com/user-attachments/assets/47daaba3-9d09-43fa-a782-a3d937efbe0e

Why

Fixes #5581. There was no way to land on the cut you are tightening in a long composition; Fit always shows the whole span.

Related work

Fixes #5581. Draft #5597 is the same button and the same Z key, and it has not moved since the day it opened. This is the live attempt. That draft should not land as well.

How

  • zoomToSelectedClips in timelineZoom.ts owns the span and the zoom percent: union of the selected clips' [start, end], fitted into viewportWidth - contentOrigin - 2×margin, clamped through clampTimelineZoomPercent. It returns start, end, and zoomPercent. Scroll stays in the timeline zoom request, which already clamps a selection at the start or the end of the content. The margin constant is shared with rangeBox (TIMELINE_RANGE_MARGIN_PX), and easeToRange derives its zoom percent from this function so the range zoom and zoom-to-selection cannot drift apart.
  • The toolbar button and Z use the same resolution as the action. A selection has to match a timeline element (key ?? id) with a positive duration. An expanded sub-composition child, which selects as file#id, and a selection whose clips all have zero duration leave the button disabled. Z is not consumed in those cases, so it is not a silent no-op. Shift+Z keeps redo.
  • zoomTimelineToRange, exported from the package index, resolves "cancelled" when the timeline viewport is not wider than its headers. A collapsed timeline has no track to fit. That is pinned in timelineZoomInput.test.ts.
  • The shortcut is listed under Editing in the default shortcuts panel. The toolbar button sits before Fit.

Test plan

How was this tested?

  • Unit tests added/updated — timelineZoom.test.ts covers the span, the zoom percent, a zero-length entry, and a collapsed viewport. TimelineToolbar.test.tsx disables the button for no selection, a file#id selection, and a zero-duration clip, and the mounted timeline test asserts the real scroll for a mid-span clip, a clip at time 0, and a clip at the end of the fit span. timelineZoomInput.test.ts pins zoomTimelineToRange resolving "cancelled" when the timeline is no wider than its headers. useAppHotkeys.test.ts covers Z consuming the key for a resolvable clip and leaving it alone otherwise.
  • Manual testing performed — the studio tests for these files pass. A studio recording checks the disabled tooltip, the Z tooltip, the last-clip zoom, and the clip at time 0 staying at scroll 0.
  • Documentation updated (if applicable)
  • Comments follow CONTRIBUTING.md "Comments": they say why, not what, and a bug fix says what the code must do and how to reproduce the bug

@DanielMcSheehy
DanielMcSheehy force-pushed the feat/timeline-zoom-to-selection branch 2 times, most recently from 626b77b to 4a3f5b3 Compare October 18, 2026 06:06
@DanielMcSheehy

Copy link
Copy Markdown
Author

@miguel-heygen could I trouble you for a review? :)

@DanielMcSheehy
DanielMcSheehy force-pushed the feat/timeline-zoom-to-selection branch from 4a3f5b3 to bdce2a0 Compare October 18, 2026 06:25

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, and for the demo video, it made the behavior easy to check. The keyboard part is right: meta and control events are routed to the modifier dispatcher before the plain key dispatcher runs, so bare Z cannot swallow undo on any platform, and the typing guard already runs first. The percent math also matches the existing range zoom exactly, so the zoom button and pinch paths are unchanged. I checked that algebraically rather than trusting it.

The blocker is the scroll half. zoomToSelectedClips computes scrollLeft, contentWidth and maxLeft, with the same bare 2 pixel fudge, but nothing in the app reads any of them: easeToRange takes only zoomPercent, and zoomTimelineToSelectedClips takes only start and end. The real clamp is the one request() already owns, which has done this since before the change. So the PR body's claim about selections at the very start or end of the timeline is true in the app but is not what this code does, and the four new tests in timelineZoom.test.ts assert against the copy, through a place() helper that re-implements the production formula inside the test file. If the timeline's track width or margin ever changes in request(), those tests keep passing while the behavior drifts. Please drop scrollLeft and the fields that feed it, keep start, end and zoomPercent, and let the mounted timeline test in the toolbar spec carry the proof. That one is the good test here.

Two smaller things. The button's enabled state asks whether anything is selected, but the action asks whether the selection resolves to a timeline element, so selecting an expanded sub composition child, which selects as file#id, leaves the button live and the click does nothing with no feedback. The same happens when every selected clip has zero duration. Driving the disabled state from the same resolution the action uses would close that. Separately, the new not greater than guard on the viewport width changes zoomTimelineToRange, which is exported from the package index, from proceeding to resolving cancelled when the timeline is collapsed. That is probably the better behavior, but it is an unflagged change to a public entry point and no test pins either side.

Worth flagging before more work goes in: there is another open pull request, 5597, on the same issue with the same title, the same button and the same Z key. It is a draft and has not moved since the day it was opened, so this one looks like the live attempt, but someone should decide which survives rather than landing both halves of the same screen.

Last note on checks. Only the mergeability and WIP jobs have run at this head, so the suite is unproven here no matter what the checks tab suggests. The branch is also a few hundred commits behind main and will need a rebase.

@DanielMcSheehy
DanielMcSheehy force-pushed the feat/timeline-zoom-to-selection branch from bdce2a0 to f735daf Compare October 24, 2026 05:42
@DanielMcSheehy

Copy link
Copy Markdown
Author

Addressed the review on f735daf, rebased onto current main.

Scroll is no longer computed beside the zoom percent. zoomToSelectedClips returns start, end, and zoomPercent only. The clamp stays in the timeline zoom request, and the mounted toolbar tests read that scroll element: a mid-span clip lands between the margins, a clip at time 0 stays at scroll 0, and a clip at the end of the fit span stays inside the viewport.

The button and Z use the same resolution as the action. A file#id selection, or a selection whose clips all have zero duration, leaves the button disabled and does not consume Z.

zoomTimelineToRange now resolves "cancelled" when the viewport is not wider than its headers. That is a change to the exported entry point, pinned in timelineZoomInput.test.ts and called out in the PR body.

New demo, from Fit: disabled tooltip, Z tooltip, last-clip zoom, then the clip at time 0 with no scroll past zero.

zoom-selection-demo.mp4

Draft #5597 is still open on the same issue with the same button and key. This is the live attempt. I have not closed that draft.

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the quick turnaround, and for rebasing. All three points from the last review are addressed:

  • zoomToSelectedClips now returns only the span and the zoom percent, the copied-formula tests are gone, and the mounted toolbar tests click the real button and read the real scroll clamp. That's the right place for the proof.
  • The button and Z now use the same check as the action, and the tests cover the file#id and zero-duration selections.
  • The "cancelled" change to zoomTimelineToRange is pinned and called out in the description.

I ran the touched studio tests at this head and they pass. Removing the resolvable-clip check from the Z handler makes the new hotkey test fail, so it's testing the real thing.

Small things, none blocking:

  1. The new collapsed-timeline guard lives in zoomToSelectedClips, which easeToRange now calls for every zoom, so it also reaches zoomTimelineStep. With the timeline no wider than its track headers, the zoom in/out buttons used to step the zoom; now they do nothing. I checked this both ways: main steps to 2x, this branch stays at fit. It's a narrow case, but it's a behavior change outside the new feature. Letting zoomTimelineStep fall back to requestTimelineZoom(nextPercent), like its no-viewport branch already does, would keep it.
  2. Z calls preventDefault() before zoomTimelineToSelectedClips() gets a chance to return false (no viewport yet, or the collapsed case). Using the boolean it already returns would keep it from becoming a silent no-op.
  3. Nit: zoomTimelineToSelectedClips builds the selection spans three times and computes a zoom percent it then discards, since zoomTimelineToRange computes its own. positiveClipSpan(selectedClipSpans(state)) followed by zoomTimelineToRange(span.start, span.end) would be enough.

The full CI suite still hasn't run at this head (only the mergeability and WIP checks), so a maintainer needs to approve the workflow run. Draft #5597 is still open on the same issue, and maintainers should decide which one stays open.

Verdict: COMMENT
Reasoning: The earlier blocker and both smaller points are fixed and the touched tests pass locally, but the required CI suite has not run at this head; approve once it is green.

@DanielMcSheehy

Copy link
Copy Markdown
Author

The three follow-ups are in 59c6529.

  1. Zoom in/out no longer dies when the timeline is no wider than its headers. That step uses requestTimelineZoom, the same path a missing viewport already used, so it still doubles the scale. Pinned in timelineZoomInput.test.ts.
  2. Z calls zoomTimelineToSelectedClips() first and consumes the key only when that returns true. No viewport, a collapsed timeline, a file#id selection, and a zero-duration clip all leave the key alone.
  3. Zoom-to-selection now takes the span once and hands start/end to zoomTimelineToRange. It no longer builds the span three times or throws away a percent.

Draft #5597 is still the maintainers' call. I can't approve the workflow run on this repo.

@DanielMcSheehy

Copy link
Copy Markdown
Author

Demo after the follow-ups. Fit, disabled tooltip, last-clip zoom, zoom-in stepping to 200%, then Z on the clip at time 0.

zoom-selection-followup.mp4

Toolbar action and Z fit the selected clips to the visible width, with the
range margin, and stay disabled until something is selected.
The scroll clamp already lives in the timeline zoom request. Drop the
unused copy, and disable the button and Z unless the selection matches
a timeline clip with duration. A collapsed timeline cancels
zoomTimelineToRange instead of laying out a percent the view cannot show.
A collapsed timeline has no track for a range zoom, so zoom in and out
step the percent directly. Z is consumed only when that zoom actually
starts, and zoom-to-selection no longer builds the span three times.
@DanielMcSheehy
DanielMcSheehy force-pushed the feat/timeline-zoom-to-selection branch from 59c6529 to 2abcdac Compare October 25, 2026 03:42

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the follow-ups. All three points from my last review are fixed at 2abcdacb:

  1. On a timeline no wider than its track headers, zoom in/out falls back to requestTimelineZoom, so the step still changes the scale. The new test pins it.
  2. Z is only consumed when the zoom actually starts. The hotkey test now covers the no-viewport and collapsed cases as well.
  3. Zoom-to-selection builds the span once and hands it to zoomTimelineToRange.

I ran the touched studio tests at this head on Linux (hotkeys, zoom input, zoom math and the toolbar), and they pass three times in a row. Two checks to confirm the new tests are real: removing the collapsed fallback from zoomTimelineStep fails the new step test, and moving preventDefault() back in front of the zoom call fails the Z test.

The full CI suite still hasn't run here, since fork workflows need a maintainer to start them, so that has to go green before this merges.

Verdict: APPROVE
Reasoning: Every earlier point is fixed and covered by tests that fail without the fix; the only open item is starting CI for the fork.

The approved run was cancelled during checkout, so the required checks
never executed.
@DanielMcSheehy

Copy link
Copy Markdown
Author

@miguel-heygen thanks so much for your feedback. looks like a maintainer needs to run the ci again. it passed locally

@jrusso1020

Copy link
Copy Markdown
Collaborator

Looks good, we want it. Your last push came after Miguel's approval, so it needs a fresh one at the new head. I've approved the CI runs.

@miguel-heygen
miguel-heygen added this pull request to the merge queue Oct 25, 2026

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approved. The source at this head matches what Miguel approved at 2abcdac, and the earlier scroll-math, selection and Z-key fixes all still hold.

Merged via the queue into heygen-com:main with commit d787681 Oct 26, 2026
63 checks passed
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.

Timeline: zoom to the selected clips

3 participants