/** * Pure scroll geometry for Scroller — no DOM, no Svelte, so every rule below is * unit-testable in the fast `node` suite. * * All positions are **content coordinates**: the distance from the scroll * container's content origin, i.e. what `scrollLeft` addresses. The component * measures them with `offsetLeft`/`offsetWidth` — the transform-free layout * API — because `getBoundingClientRect()` reports the *visual* box, which the * `emphasis` lift inflates while it is being measured (see `measure()` in * Scroller.svelte). */ /** Where an item comes to rest when the row snaps. */ export type ScrollerAlign = 'start' | 'center'; /** One item's extent along the scroll axis, in content coordinates. */ export interface ScrollerItemMetrics { /** Leading edge, measured from the content origin. */ start: number; /** Extent along the scroll axis. */ size: number; } /** * Index of the item the row is currently resting on — what the dots mark with * `aria-current` and what a `center` step navigates from. * * Compares against each item's **reachable** scroll target, not its raw anchor, * and that distinction is the whole point. With `align="start"` the last items * sit further right than `scrollLeft` can ever reach: a five-item row 1344px * wide in a 664px viewport tops out at 680, while item 5 begins at 1088. Judging * by the raw anchor made those items permanently unreachable, so their dots * never lit up — at the right-hand end the row highlighted item 3 while items 4 * and 5 were the ones on screen, and clicking dot 5 visibly landed elsewhere. * Clamping each target into `[0, maxScroll]` puts every dot back in play. * * Ties resolve to the LATER item: at the end of the row several items share the * clamped target `maxScroll`, and the one the user has scrolled *to* is the last * of them. (Mid-row ties — resting exactly between two neighbours — likewise * resolve forwards, in the direction of travel.) Deterministic either way, so * the indicator never flickers. * * Returns `-1` for an empty row so callers cannot mistake "nothing here" for * "the first one". */ export declare function activeItemIndex(items: readonly ScrollerItemMetrics[], scrollStart: number, viewportSize: number, align: ScrollerAlign, maxScroll: number): number; /** * One place the row can come to rest — what a dot stands for. * * On a start-aligned row the last items begin further right than `scrollLeft` * can reach, so their targets all clamp to `maxScroll`: several items, one * resting place. A dot per ITEM then lies twice over — clicking the dot for a * collapsed item lands at the shared target, and the mark jumps to a different * dot than the one just pressed. A dot per RESTING PLACE cannot lie: every dot * has its own destination, and the collapsed tail becomes one dot that covers * `firstIndex…lastIndex` (its label says so). A centred row pads its track so * every item's target is reachable — there the grouping is the identity and * nothing changes. */ export interface ScrollerRestingPosition { /** Scroll position this dot travels to, already confined to the reachable range. */ target: number; /** Index of the first item resting here. */ firstIndex: number; /** Index of the last item resting here — equal to `firstIndex` except for the collapsed tail. */ lastIndex: number; } /** * The distinct places the row can rest, in scroll order. Items whose clamped * targets coincide (within `epsilon`, absorbing sub-pixel layout) share one * entry. Targets are monotonically increasing with the item order, so only * neighbouring items can ever share a place — grouping runs over consecutive * items. */ export declare function restingPositions(items: readonly ScrollerItemMetrics[], viewportSize: number, align: ScrollerAlign, maxScroll: number, epsilon?: number): ScrollerRestingPosition[]; /** * Scroll position that brings `index` to the snap anchor — the target of a dot * click. * * Deliberately NOT clamped to the scrollable range: `scrollTo` clamps for us, * and clamping here would need `scrollWidth`, which this module has no business * knowing. For a `center` row the first and last items only reach the middle * because the component pads the track (see `scroller.variants.ts`); without * that padding the browser's own clamp silently wins and the card rests at the * edge — the classic centred-carousel bug. */ export declare function scrollTargetForIndex(items: readonly ScrollerItemMetrics[], index: number, viewportSize: number, align: ScrollerAlign): number; /** * Scroll position for one press of the previous/next control. * * The unit of travel follows the alignment, because that is what the row's * geometry makes meaningful: * * - `start` — one **viewport** (the classic "next page" of a media row). A chip * bar with thirty narrow chips would be tedious to step through one item at a * time. * - `center` — one **item**, because in a centred row the middle IS the unit; * paging by viewport would jump straight past it. */ export declare function scrollTargetForStep(items: readonly ScrollerItemMetrics[], scrollStart: number, viewportSize: number, align: ScrollerAlign, direction: 1 | -1, maxScroll: number): number; /** * Whether the row is scrolled to its leading / trailing end — drives the * disabled state of the controls. * * `epsilon` absorbs sub-pixel scroll positions: browsers report fractional * `scrollLeft` on fractional layouts and on zoomed viewports, so an exact * `scrollStart >= scrollSize - viewportSize` comparison would leave the "next" * button enabled at the very end, scrolling nowhere. */ export declare function scrollEdges(scrollStart: number, scrollSize: number, viewportSize: number, epsilon?: number): { atStart: boolean; atEnd: boolean; };