import { clamp } from "../slider-utils/clamp"; /** * Sub-pixel tolerance around each edge. Scroll offsets within one pixel of * 0 or the maximum are treated as "at the edge" — browsers occasionally * report fractional offsets due to device-pixel rounding even when the * user perceives the content to be fully scrolled. */ export const SCROLL_EDGE_TOLERANCE_PX = 1; /** * Maximum scroll offset available for the given scroll and client sizes. * Guards against negative values — when content is smaller than the * viewport, the max is 0, not a negative number. */ export function getMaxScrollOffset( scrollSize: number, clientSize: number, ): number { return Math.max(0, scrollSize - clientSize); } /** * Clamps `value` into `[0, max]` and snaps to whichever edge is within * {@link SCROLL_EDGE_TOLERANCE_PX}. When both edges are within tolerance * (i.e. `max <= SCROLL_EDGE_TOLERANCE_PX`) the closer edge wins, with ties * going to the start. */ export function normalizeScrollOffset(value: number, max: number): number { if (max <= 0) { return 0; } const clamped = clamp(value, 0, max); const startDistance = clamped; const endDistance = max - clamped; const withinStartTolerance = startDistance <= SCROLL_EDGE_TOLERANCE_PX; const withinEndTolerance = endDistance <= SCROLL_EDGE_TOLERANCE_PX; if (withinStartTolerance && withinEndTolerance) { return startDistance <= endDistance ? 0 : max; } if (withinStartTolerance) { return 0; } if (withinEndTolerance) { return max; } return clamped; }