import { Pause, Play, RotateCcw, StepBack, StepForward } from "lucide-react"; import { useSyncExternalStore, type ReactNode } from "react"; import type { Timeline, TimelineSnapshot } from "./Timeline"; // Styled to match @elvis/ui's StepControls, which works in whole iterations const thumbClass = "[&::-webkit-slider-thumb]:size-4 [&::-webkit-slider-thumb]:cursor-grab [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-white [&::-webkit-slider-thumb]:bg-ink " + "[&::-moz-range-thumb]:size-4 [&::-moz-range-thumb]:cursor-grab [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-white [&::-moz-range-thumb]:bg-ink"; // How far an arrow key moves the playhead const KEY_MOVE_MS = 250; // The slider's thumb stops this far from each end, so marks on the track are inset to line up with it const THUMB_RADIUS = 8; const iconButtonClass = "flex size-11 shrink-0 items-center justify-center rounded border-[1.5px] border-ink bg-white text-ink enabled:cursor-pointer enabled:hover:bg-marker-soft disabled:cursor-not-allowed disabled:opacity-40 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ink"; // The slider along the whole run, with a tick for each step and the two passes marked function TimelineBar({ timeline, snapshot }: { timeline: Timeline; snapshot: TimelineSnapshot }) { const time = useSyncExternalStore(timeline.subscribeTime, timeline.getTime); const { ticks, backwardAt, iteration, lastIteration } = snapshot; const length = ticks[ticks.length - 1]; const at = (t: number) => `${length > 0 ? (t / length) * 100 : 0}%`; return (
{/* Inset so positions here match the thumb's centre */}
{ticks.map((tick) => (
))} {backwardAt !== null && (
)}
timeline.seek(Number(e.target.value))} onKeyDown={(e) => { if (e.key === "ArrowLeft" || e.key === "ArrowRight") { e.preventDefault(); timeline.seek(time + (e.key === "ArrowLeft" ? -KEY_MOVE_MS : KEY_MOVE_MS)); } }} className={`absolute inset-0 m-0 w-full cursor-pointer appearance-none bg-transparent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ink disabled:cursor-not-allowed ${thumbClass}`} />
{backwardAt !== null && (
Forward pass Backward pass
)}
); } export default function TimelineControls({ timeline, snapshot, animated, onAnimatedChange, }: { timeline: Timeline; snapshot: TimelineSnapshot; animated: boolean; onAnimatedChange: (animated: boolean) => void; }) { const { iteration, lastIteration, atStart, playing } = snapshot; const atEnd = lastIteration === null || iteration >= lastIteration; return (

iteration {iteration}{lastIteration !== null ? ` / ${lastIteration}` : ""}

); } function IconButton({ label, onClick, disabled, children }: { label: string; onClick: () => void; disabled: boolean; children: ReactNode }) { return ( ); }