fix(studio): clamp the timeline scrub to 0 instead of dropping it

Dragging the playhead to the start of the composition needed a very slow
drag. The scrub surface begins GUTTER + TRACKS_LEFT_PAD px right of the
viewport edge, and both scrub paths bailed out when the pointer sat left of
that origin rather than clamping. So the last 80px of the drag toward zero
silently did nothing: the playhead stuck at whatever the last in-range sample
reported, and only a drag slow enough to sample inside the thin sliver before
the origin ever reached 0.

Both paths now share getTimelineScrubTime, which clamps to [0, duration]. One
owner, so the live-feedback path and the committed-seek path cannot disagree
about the edge again.
This commit is contained in:
Miguel Angel Simon Sierra
2026-07-27 19:09:16 +02:00
parent 3c7400af89
commit b386b55f73
4 changed files with 85 additions and 9 deletions
@@ -7,7 +7,7 @@ import {
} from "./timelineEditing";
import type { TimelineElement } from "../store/playerStore";
import { liveTime, usePlayerStore } from "../store/playerStore";
import { GUTTER, TRACKS_LEFT_PAD } from "./timelineLayout";
import { GUTTER, TRACKS_LEFT_PAD, getTimelineScrubTime } from "./timelineLayout";
import {
computeMarqueeSelection,
getMarqueeRect,
@@ -286,11 +286,15 @@ export function useTimelineRangeSelection({
const el = scrollRef.current;
if (el) {
const rect = el.getBoundingClientRect();
const x = clientX - rect.left + el.scrollLeft - GUTTER - TRACKS_LEFT_PAD;
if (x >= 0) {
const dur = el.scrollWidth / pps;
liveTime.notify(Math.max(0, Math.min(dur, x / pps)));
}
liveTime.notify(
getTimelineScrubTime({
clientX,
viewportLeft: rect.left,
scrollLeft: el.scrollLeft,
pixelsPerSecond: pps,
duration: el.scrollWidth / pps,
}),
);
}
if (!seekRafRef.current) {
seekRafRef.current = requestAnimationFrame(() => {