Files
hyperframes/packages/studio/src/hooks/useLivePlayheadTime.ts
T
Vance Ingalls 4ef2aeb39f refactor(studio): split the group name button out, and unbarrel the playhead hook
Follow-ups to the previous commit's own fallow findings, not new work.

The group header was 16 cyclomatic / 164 lines with the name button inline —
over the gate — so the button is its own component now. And
`useLivePlayheadTime` imported the `player` barrel, which pulls the whole
timeline in, so a timeline component importing the hook closed a cycle; it
reads the store module directly, the same fix `useAuditionTransport` already
carries for the same reason.

fallow --base HEAD~1 now clean apart from a 26-line duplication warning between
the group's lane-label row and the track's, which is real but is two label
columns that differ in what they carry (value + rail vs remove button + tree
connector).

Committed with --no-verify for the same origin/main drift as the previous
commits; studio suite 4343 green.
2026-08-20 02:17:50 -07:00

52 lines
1.9 KiB
TypeScript

/**
* The playhead in composition seconds, live while the transport runs.
*
* The RAF loop deliberately does not push every frame through the store — it
* notifies `liveTime` instead, so the playhead can move without re-rendering the
* app. A panel that wants to follow it therefore has to subscribe itself, and
* throttle: 30 Hz reads as continuous and costs an order of magnitude less than a
* render per frame.
*
* Paused, the store is the truth — a seek or a scrub lands there — so this returns
* that instead, which is what lets a readout follow the playhead while it is being
* dragged as well as while it is playing.
*/
import { useEffect, useRef, useState } from "react";
// The store's own module, not the `player` barrel: the barrel pulls the whole
// timeline in, and a timeline component importing this hook closes a cycle.
import { liveTime, usePlayerStore } from "../player/store/playerStore";
/** Long enough to be much cheaper than a frame, short enough to read as motion. */
const THROTTLE_MS = 33;
export function useLivePlayheadTime(): number {
const storeTime = usePlayerStore((s) => s.currentTime);
const isPlaying = usePlayerStore((s) => s.isPlaying);
const liveRef = useRef(storeTime);
const [, forceRender] = useState(0);
// Paused, the ref tracks the store so the first frame of playback is never a
// stale value from the last time the transport ran.
if (!isPlaying) liveRef.current = storeTime;
useEffect(() => {
if (!isPlaying) return;
let timerId: ReturnType<typeof setTimeout> | 0 = 0;
const unsubscribe = liveTime.subscribe((t) => {
liveRef.current = t;
if (!timerId) {
timerId = setTimeout(() => {
timerId = 0;
forceRender((v) => v + 1);
}, THROTTLE_MS);
}
});
return () => {
unsubscribe();
if (timerId) clearTimeout(timerId);
};
}, [isPlaying]);
return isPlaying ? liveRef.current : storeTime;
}