import { onCleanup } from "solid-js"; type AnimationFrameId = number; /** * Class-instance-like animation-frame handle with `request` / `cancel`. * * Mirrors Base UI React's `AnimationFrame` * (`@base-ui/utils/useAnimationFrame` — * `packages/utils/src/useAnimationFrame.ts`) so component ports can read 1:1 * against the React source. * * React's version bounces through an internal scheduler for `O(1)` cancel * ergonomics and fake-timer resilience; here we delegate directly to * `requestAnimationFrame` / `cancelAnimationFrame`. The public handle shape * (`request(fn)`, `cancel()`, `isStarted()`) is identical. */ export class AnimationFrame { static create(): AnimationFrame { return new AnimationFrame(); } static request(fn: FrameRequestCallback): AnimationFrameId { return requestAnimationFrame(fn); } static cancel(id: AnimationFrameId): void { cancelAnimationFrame(id); } private currentId: AnimationFrameId | null = null; /** * Requests `fn` on the next animation frame, cancelling any previously * requested frame from this handle first. */ request(fn: () => void): void { this.cancel(); this.currentId = requestAnimationFrame(() => { this.currentId = null; fn(); }); } isStarted(): boolean { return this.currentId !== null; } cancel = (): void => { if (this.currentId !== null) { cancelAnimationFrame(this.currentId); this.currentId = null; } }; } /** * A `requestAnimationFrame` with automatic cleanup via `onCleanup`. * * Returns an {@link AnimationFrame} handle whose `request(fn)` schedules `fn` * on the next frame (cancelling any previously requested frame) and whose * `cancel()` cancels any pending frame. On disposal of the owning scope, * `cancel()` fires automatically so outstanding frames don't leak. * * Must be called from an owned scope so `onCleanup` has somewhere to * register. */ export function useAnimationFrame(): AnimationFrame { const frame = AnimationFrame.create(); onCleanup(frame.cancel); return frame; }