SheetXL - v0.8.0-beta.9+sites.210.e2d420901
    Preparing search index...

    The scroll store + writer + frame loop. Owns scroll position as a plain external store (not React state): one synchronous source of truth that React reads via useSyncExternalStore.

    Input sets a target (setTarget); a single rAF ticker advances current toward target each frame, emits to subscribers when current moves, and self-stops when settled. Input never triggers a render directly (it only writes a number), so input rate is decoupled from render rate and bursts/jank can't flood.

    Animation is per-request (setTarget(l, t, true)), standard smooth-scroll semantics: fixed-duration, time-based ease-out — every animated scroll takes the same wall-clock time (SCROLL_ANIMATION_MS) whether it's a PageDown or a goto across 1M rows, because long jumps are clamped: the glide starts within ANIMATION_CLAMP_VIEWPORTS viewports of the target (the rows in between are never painted). Deltas smaller than the ramp finish proportionally sooner. Time-based ⇒ frame-rate independent (identical feel at 60/120Hz). Un-animated targets apply in one step (the classic immediate scroll).

    Owns clamping too: setBounds takes the structural bounds (the grid pushes them) and setTarget clamps every target into them — the grid no longer clamps.

    Implements

    Index
    • The raw cell-area client size = the measured client (uncapped by content). The grid reads this for its own pane layout (min(client, content)); getMetrics caps it at content.

      Returns Size

    • The combined viewport (position + measured sizes) as one identity-stable object — what a subscribed ScrollPane reads to size and place its scrollbars.

      Returns ViewportMetrics

    • Convenience { left, top } of the current (rendered) scroll.

      Returns TopLeft

    • The current (rendered) scroll snapshot. The returned object is identity-stable until the scroll changes, so it is safe as a useSyncExternalStore snapshot.

      Returns TopLeft

    • The scroll TARGET the ticker is easing toward (equals current when settled).

      Returns TopLeft

    • The zoom factor (1 = none). Part of the viewport shape so a shared/synced viewport carries it.

      Returns number

    • Whether the viewport has everything it needs to clamp + emit: a measured client (observer fired or manual sizes pushed) AND real content AND a positive client. The shared readiness signal that replaces each grid's "pending until ready" gate.

      Returns boolean

    • Readiness for a SINGLE axis — measured client + real content along that axis only. A composed viewport is ready when each of its two source controllers is ready on the axis it contributes, so an own/throwaway controller with only one meaningful axis doesn't block on the other.

      Parameters

      • axis: "height" | "width"

      Returns boolean

    • Whether the current (rendered) position has reached the target — no easing in progress.

      Returns boolean

    • START measuring: attach a ResizeObserver to el (the scroll-client box) so the viewport owns its own client-size measurement and derives its clamp bounds internally. Returns an unobserve fn — symmetric with subscribe/unsubscribe, and the ONLY teardown the viewport has (there is no dispose; the rAF ticker is self-terminating and listeners each remove themselves).

      Parameters

      • el: Element | null

      Returns () => void

      Each call is SELF-CONTAINED (its own observer, its own detach), so multiple elements can observe the same viewport concurrently — e.g. split peers each reporting the shared axis (they report the same value by construction; last write wins). A caller tears down exactly what it set up, nothing else — StrictMode mount→cleanup→mount is naturally symmetric. Guarded for absence exactly like the rAF ticker: with no ResizeObserver (SSR / non-DOM tests) it resolves an immediate default client so isReady still becomes true. null is a no-op (returns a no-op detach).

    • Observe el but update ONLY the given axis of the measured client (the other axis is left as-is). Self-contained per call like observe. Returns an unobserve fn.

      Parameters

      • el: Element | null
      • axis: "height" | "width"

      Returns () => void

    • Relay a zoom-gesture step to the zoom's owner. The ScrollPane that runs the gesture calls this (as it calls setTarget for a scroll); the viewport does NOT apply it — zoom is a whole percent owned by a model the viewport knows nothing about — it hands the step to subscribeZoom listeners, who commit it and push the result back in through setZoom.

      Parameters

      Returns void

    • Set the clamp bounds (structural — total size, viewport size, frozen floors). Re-clamps the current target into the new bounds (e.g. the viewport shrank) and eases to it. Pushed by the grid; the controller, not the grid, owns the clamp.

      Parameters

      Returns void

    • Declare the content geometry (the host — a grid — pushes this): the total scroll extent plus the frozen-pane floors (min*, the same minLeft/minTop a manual setBounds carries). Calling it switches the viewport into DERIVED-bounds mode — it computes its own bounds from content − client and no longer needs the host to push setBounds. Emits + re-derives.

      Parameters

      • totalWidth: number
      • totalHeight: number
      • minLeft: number = 0
      • minTop: number = 0

      Returns void

    • Declare content for ONE axis only (the other axis's content is left untouched).

      Parameters

      • axis: "height" | "width"
      • total: number
      • min: number = 0

      Returns void

    • Push the measured viewport sizes (the grid measures them where it pushes setBounds; a subscribed ScrollPane reads them via getViewport to size its scrollbar thumbs). Emits on change.

      Parameters

      • width: number
      • height: number
      • totalWidth: number
      • totalHeight: number

      Returns void

      This is the MANUAL input path — used by a standalone ScrollPane driving arbitrary content (no grid to declare setContent). Calling it marks the viewport "manual provenance": the manually pushed sizes/bounds win over anything observe/setContent would derive.

    • Set the scroll TARGET (clamped) and start the ticker if needed.

      Parameters

      • left: number
      • top: number
      • Optionalanimate: boolean

        Animation is a property of the REQUEST, not a global policy: true glides to the target (fixed ~SCROLL_ANIMATION_MS ease-out, long jumps clamped to animate only the arrival — a wheel notch, PageDown, or goto). Omit/false for position control (a thumb drag, the touch scroller's per-frame writes, a model restore) — the classic immediate scroll.

      Returns boolean

      true if the target changed (caller fires its immediate-scroll side effects), else false (a no-op move — caller bails).

    • Set the zoom factor and notify subscribers on a change. Zoom is structural (it changes every pane's geometry), so the grid re-renders when this emits. The scroll position (current + target) is rescaled by the zoom ratio so the same cell stays anchored at the top-left and so scroll / zoom (the zoom-independent base position) is preserved. Because zoom and scroll are mutated together before the single emit, a listener that reads scroll on this emit sees screen px already consistent with the new zoom.

      With left/top (screen px for the NEW zoom) the position is set to them instead of the ratio-rescale, current and target alike, in the same single emit — a focal-point zoom lands zoom and scroll together, with no frame painted at the top-left-anchored position in between.

      Parameters

      • zoom: number
      • Optionalleft: number
      • Optionaltop: number

      Returns void

    • Subscribe to scroll changes. Driven by the rAF ticker, so the listener fires at most once per animation frame (and only while current is advancing toward target). Returns a remover.

      Parameters

      • listener: () => void

      Returns () => void