{"mappings":"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","sources":["packages/react-stately/src/virtualizer/ScrollAnchor.ts"],"sourcesContent":["/*\n * Copyright 2026 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {Key} from '@react-types/shared';\nimport {LayoutInfo} from './LayoutInfo';\nimport {Rect, RectCorner} from './Rect';\nimport {Size} from './Size';\n\nexport type ScrollAnchorAxis = 'x' | 'y';\nexport type ScrollAnchorEdge = 'start' | 'end';\nexport interface ScrollAnchor {\n  key: Key;\n  corner: RectCorner;\n  offset: number;\n}\n\nexport interface ScrollAnchorInfo {\n  /** Which edge of the content the viewport should stay anchored to. */\n  edge: 'start' | 'end';\n  /** Which axis `edge` refers to — 'y' for vertical lists, 'x' for horizontal. */\n  axis: 'x' | 'y';\n  /** Distance (px) from `edge` within which the viewport is considered \"following\" it. */\n  threshold: number;\n  /**\n   * Optional classifier excluding structural/ephemeral layout infos (e.g. loaders) from being\n   * selected as the anchor. Defaults to allowing any layoutInfo.\n   */\n  isAnchorable?: (layoutInfo: LayoutInfo) => boolean;\n}\n\n/**\n * Minimum overlap an item must have with the viewport, along the scroll axis,\n * to be eligible as a scroll anchor. Without this, an item that only overlaps the viewport\n * by a sliver (e.g. 1px, essentially scrolled out of view) can still \"win\" the anchor\n * tie-break over a substantially visible item.\n */\nconst MIN_ANCHOR_OVERLAP = 4;\n\nfunction dimensionForAxis(axis: ScrollAnchorAxis): 'width' | 'height' {\n  return axis === 'x' ? 'width' : 'height';\n}\n\n/**\n * Given a previously-captured anchor, computes the new viewport coordinate (along `axis`) needed\n * to keep it at the same offset from the viewport's start.\n */\nexport function computeScrollAnchorTarget(\n  anchor: ScrollAnchor,\n  axis: ScrollAnchorAxis,\n  getLayoutInfo: (key: Key) => LayoutInfo | null,\n  visibleRect: Rect,\n  contentSize: Size\n): number | null {\n  let finalInfo = getLayoutInfo(anchor.key);\n  if (!finalInfo) {\n    return null;\n  }\n  let adjustment = finalInfo.rect[anchor.corner][axis] - visibleRect[axis] - anchor.offset;\n  if (adjustment === 0) {\n    return null;\n  }\n  let target = visibleRect[axis] + adjustment;\n  let dimension = dimensionForAxis(axis);\n  let max = Math.max(0, contentSize[dimension] - visibleRect[dimension]);\n  let clamped = Math.max(0, Math.min(max, target));\n  return clamped !== visibleRect[axis] ? clamped : null;\n}\n\n/**\n * Picks the item to anchor scroll to: the one nearest the top of the viewport when\n * anchoring to 'end', or nearest the bottom when anchoring to 'start'. Callers can\n * exclude certain items (like loaders) with `isAnchorable`.\n */\nexport function captureScrollAnchor(\n  edge: ScrollAnchorEdge,\n  axis: ScrollAnchorAxis,\n  visibleRect: Rect,\n  visibleLayoutInfos: Iterable<[Key, LayoutInfo]>,\n  isAnchorable: (layoutInfo: LayoutInfo) => boolean = () => true\n): ScrollAnchor | null {\n  let dimension = dimensionForAxis(axis);\n  let best: ScrollAnchor | null = null;\n  for (let [key, layoutInfo] of visibleLayoutInfos) {\n    if (!layoutInfo || !isAnchorable(layoutInfo)) {\n      continue;\n    }\n    let overlap = layoutInfo.rect.intersection(visibleRect)[dimension];\n    if (layoutInfo.rect.area > 0 && overlap >= MIN_ANCHOR_OVERLAP) {\n      let corner = layoutInfo.rect.getCornerInRect(visibleRect) ?? 'topLeft';\n      let offset = layoutInfo.rect[corner][axis] - visibleRect[axis];\n      let isBetter = !best || (edge === 'end' ? offset < best.offset : offset > best.offset);\n      if (isBetter) {\n        best = {key, corner, offset};\n      }\n    }\n  }\n  return best;\n}\n\n/** Returns the viewport coordinate (along `axis`) that pins the viewport to `edge` of the content. */\nexport function getEdgeSnapTarget(\n  edge: ScrollAnchorEdge,\n  axis: ScrollAnchorAxis,\n  contentSize: Size,\n  previousVisibleRect: Rect\n): number {\n  if (edge === 'start') {\n    return 0;\n  }\n  let dimension = dimensionForAxis(axis);\n  return Math.max(0, contentSize[dimension] - previousVisibleRect[dimension]);\n}\n\n/**\n * Whether the viewport is currently within `threshold` px of the anchored edge — used by\n * Virtualizer to compute wasNearAnchorEdge generically, without any layout-specific state.\n */\nexport function isNearEdge(\n  visibleRect: Rect,\n  contentSize: Size,\n  edge: ScrollAnchorEdge,\n  axis: ScrollAnchorAxis,\n  threshold: number\n): boolean {\n  if (edge === 'start') {\n    return visibleRect[axis] <= threshold;\n  }\n  let dimension = dimensionForAxis(axis);\n  let distanceFromEnd = contentSize[dimension] - (visibleRect[axis] + visibleRect[dimension]);\n  return distanceFromEnd <= threshold;\n}\n\n/**\n * Works out the new scroll position after content changes. Tries to keep the anchor\n * item where it was. If that doesn't apply, sticks the view to the edge instead, but\n * only if the user was already near the edge and didn't just scroll away on their own.\n */\nexport function resolveScrollAdjustment(\n  edge: ScrollAnchorEdge,\n  axis: ScrollAnchorAxis,\n  anchor: ScrollAnchor | null,\n  wasNearAnchorEdge: boolean,\n  isScrolling: boolean,\n  itemSizeChanged: boolean,\n  contentSizeDelta: number,\n  getLayoutInfo: (key: Key) => LayoutInfo | null,\n  previousVisibleRect: Rect,\n  contentSize: Size\n): Rect | null {\n  let withTarget = (target: number): Rect =>\n    axis === 'x'\n      ? new Rect(\n          target,\n          previousVisibleRect.y,\n          previousVisibleRect.width,\n          previousVisibleRect.height\n        )\n      : new Rect(\n          previousVisibleRect.x,\n          target,\n          previousVisibleRect.width,\n          previousVisibleRect.height\n        );\n\n  if (anchor) {\n    let target = computeScrollAnchorTarget(\n      anchor,\n      axis,\n      getLayoutInfo,\n      previousVisibleRect,\n      contentSize\n    );\n    if (target != null) {\n      return withTarget(target);\n    }\n  }\n\n  if (wasNearAnchorEdge && !isScrolling && (!itemSizeChanged || contentSizeDelta > 0)) {\n    let target = withTarget(getEdgeSnapTarget(edge, axis, contentSize, previousVisibleRect));\n    return target.equals(previousVisibleRect) ? null : target;\n  }\n\n  return null;\n}\n\nexport interface ResolveAfterLayoutOptions {\n  anchorInfo: ScrollAnchorInfo | null;\n  /** The anchor captured by `captureBeforeLayout` before this pass's `layout.update()` ran. */\n  anchor: ScrollAnchor | null;\n  /** The full post-layout visible layout infos, i.e. `virtualizer.getVisibleLayoutInfos()`. */\n  postLayoutInfos: Map<Key, LayoutInfo>;\n  previousVisibleRect: Rect;\n  previousContentSize: Size;\n  contentSize: Size;\n  itemSizeChanged: boolean;\n  isScrolling: boolean;\n  getLayoutInfo: (key: Key) => LayoutInfo | null;\n}\n\n/**\n * Tracks the cross-pass state needed to keep the viewport anchored to a layout's edge across\n * relayouts.\n */\nexport class ScrollAnchorTracker {\n  private hasSnappedToEdge = false;\n  private hadEstimatedVisibleItems = false;\n  private wasNearAnchorEdge = false;\n\n  /** Resets all tracked state, e.g. when the virtualizer's layout instance changes. */\n  reset(): void {\n    this.hasSnappedToEdge = false;\n    this.hadEstimatedVisibleItems = false;\n    this.wasNearAnchorEdge = false;\n  }\n\n  /**\n   * Captures the anchor from pre-layout view positions.\n   */\n  captureBeforeLayout(\n    anchorInfo: ScrollAnchorInfo | null,\n    preLayoutInfos: Iterable<[Key, LayoutInfo]>,\n    visibleRect: Rect\n  ): ScrollAnchor | null {\n    if (!anchorInfo) {\n      return null;\n    }\n    return captureScrollAnchor(\n      anchorInfo.edge,\n      anchorInfo.axis,\n      visibleRect,\n      preLayoutInfos,\n      anchorInfo.isAnchorable\n    );\n  }\n\n  /**\n   * Runs the full post-layout decision: updates the tracked state for the next pass, and\n   * returns the resolved scroll target, or null if nothing should change.\n   */\n  resolveAfterLayout(options: ResolveAfterLayoutOptions): Rect | null {\n    let {\n      anchorInfo,\n      anchor,\n      postLayoutInfos,\n      previousVisibleRect,\n      previousContentSize,\n      contentSize,\n      itemSizeChanged,\n      isScrolling,\n      getLayoutInfo\n    } = options;\n\n    if (!anchorInfo) {\n      return null;\n    }\n\n    // Read the previous pass's state into locals before any writes below overwrite it.\n    let wasSettlingLastPass = this.hadEstimatedVisibleItems;\n    let wasNearAnchorEdgeLastPass = this.wasNearAnchorEdge;\n\n    let hasEstimated = false;\n    for (let layoutInfo of postLayoutInfos.values()) {\n      if (layoutInfo.estimatedSize) {\n        hasEstimated = true;\n        break;\n      }\n    }\n    this.hadEstimatedVisibleItems = hasEstimated;\n    // Don't recheck \"near edge?\" mid-resize because it could look like a scroll that never happened.\n    // Reuse the answer from before the resizing started.\n    if (!wasSettlingLastPass) {\n      this.wasNearAnchorEdge = isNearEdge(\n        previousVisibleRect,\n        previousContentSize,\n        anchorInfo.edge,\n        anchorInfo.axis,\n        anchorInfo.threshold\n      );\n    }\n\n    if (previousVisibleRect.area === 0) {\n      return null;\n    }\n\n    let dimension = anchorInfo.axis === 'x' ? 'width' : 'height';\n    let contentSizeDelta = contentSize[dimension] - previousContentSize[dimension];\n    let isFirstAnchoredLayout = !this.hasSnappedToEdge;\n    this.hasSnappedToEdge = true;\n\n    // Only modify scroll when content actually changed (or this is the first layout, which always snaps)\n    if (!(isFirstAnchoredLayout || contentSizeDelta !== 0 || itemSizeChanged)) {\n      return null;\n    }\n\n    let wasNearAnchorEdge =\n      isFirstAnchoredLayout ||\n      (wasSettlingLastPass && wasNearAnchorEdgeLastPass) ||\n      isNearEdge(\n        previousVisibleRect,\n        previousContentSize,\n        anchorInfo.edge,\n        anchorInfo.axis,\n        anchorInfo.threshold\n      );\n    // A first-ever layout always snaps to the edge, even if the raw distance check says\n    // otherwise. Save that real decision here so later passes in this cascade reuse it.\n    if (!wasSettlingLastPass) {\n      this.wasNearAnchorEdge = wasNearAnchorEdge;\n    }\n    // Skip restoring to the captured anchor while still resizing because items above it are also still growing,\n    // and following it would fall short of the edge.\n    let effectiveAnchor =\n      isFirstAnchoredLayout || (wasSettlingLastPass && wasNearAnchorEdgeLastPass) ? null : anchor;\n    return resolveScrollAdjustment(\n      anchorInfo.edge,\n      anchorInfo.axis,\n      effectiveAnchor,\n      wasNearAnchorEdge,\n      isScrolling,\n      itemSizeChanged,\n      contentSizeDelta,\n      getLayoutInfo,\n      previousVisibleRect,\n      contentSize\n    );\n  }\n}\n"],"names":[],"version":3,"file":"ScrollAnchor.cjs.map"}