{"version":3,"file":"useScrollEdges.mjs","names":[],"sources":["../../../../package/common/hooks/useScrollEdges.ts"],"sourcesContent":["import { useEffect, useState, type RefObject } from 'react';\n\nexport interface ScrollEdges {\n  /** Content is scrolled out of view past the element's top (or left) edge. */\n  hasStart: boolean;\n  /** Content continues past the element's bottom (or right) edge. */\n  hasEnd: boolean;\n}\n\nexport interface ScrollEdgesOptions {\n  /** Which axis to measure. Default `'vertical'`. */\n  axis?: 'vertical' | 'horizontal';\n  /** Measure at all. Default `true`; pass `false` while the element is hidden. */\n  isEnabled?: boolean;\n}\n\nconst NONE: ScrollEdges = { hasStart: false, hasEnd: false };\n\n/**\n * Reports which edges of a scroll container currently hide content, so a caller can mark the\n * boundary, most often by fading it: see `styles/scrollFade.module.pcss`.\n */\nexport function useScrollEdges(ref: RefObject<HTMLElement | null>, options: ScrollEdgesOptions = {}): ScrollEdges {\n  const { axis = 'vertical', isEnabled = true } = options;\n  const [edges, setEdges] = useState<ScrollEdges>(NONE);\n\n  useEffect(() => {\n    const element = ref.current;\n    if (!isEnabled || !element) {\n      setEdges((previous) => (previous === NONE ? previous : NONE));\n      return;\n    }\n\n    const isVertical = axis === 'vertical';\n    // Read once rather than per measurement: `getComputedStyle` in the scroll handler forces a\n    // style flush on every frame of a scroll, and the overflow mode can only change with the\n    // element's class or inline style, which re-runs this effect anyway.\n    const style = getComputedStyle(element);\n    // `hidden` and `clip` cut the overflow off with no way for anyone to reach the rest, so\n    // marking an edge there would promise a scroll that cannot happen. The metrics alone never\n    // say so: `scrollHeight` reports the full content height under both.\n    const overflow = isVertical ? style.overflowY : style.overflowX;\n    const isClipped = overflow === 'hidden' || overflow === 'clip';\n\n    const measure = () => {\n      const offset = isVertical ? element.scrollTop : element.scrollLeft;\n      const total = isVertical ? element.scrollHeight : element.scrollWidth;\n      const visible = isVertical ? element.clientHeight : element.clientWidth;\n      // A pixel of slack: fractional layout at non-integer zoom leaves the offset a hair short of\n      // the maximum, which would otherwise read as \"there is still more past this edge\" forever.\n      const hasStart = !isClipped && offset > 1;\n      const hasEnd = !isClipped && total - visible - offset > 1;\n      setEdges((previous) =>\n        previous.hasStart === hasStart && previous.hasEnd === hasEnd ? previous : { hasStart, hasEnd },\n      );\n    };\n\n    const resizes = new ResizeObserver(measure);\n    // The port's own box stops changing once it is capped, so content that grows without one of\n    // its ancestors resizing, an image decoding or a web font swapping in, only shows on a child.\n    const observeContent = () => {\n      resizes.disconnect();\n      resizes.observe(element);\n      for (const child of Array.from(element.children)) resizes.observe(child);\n    };\n\n    // Content arriving through React changes `scrollHeight` without resizing anything already\n    // observed, and the new children have to be picked up for the next time they grow.\n    const mutations = new MutationObserver(() => {\n      observeContent();\n      measure();\n    });\n\n    measure();\n    observeContent();\n    mutations.observe(element, { childList: true, subtree: true, characterData: true });\n    element.addEventListener('scroll', measure, { passive: true });\n    return () => {\n      element.removeEventListener('scroll', measure);\n      mutations.disconnect();\n      resizes.disconnect();\n    };\n  }, [ref, axis, isEnabled]);\n\n  return edges;\n}\n"],"mappings":"6DAgBA,IAAM,EAAoB,CAAE,SAAU,GAAO,OAAQ,EAAM,EAM3D,SAAgB,EAAe,EAAoC,EAA8B,CAAC,EAAgB,CAChH,GAAM,CAAE,OAAO,WAAY,YAAY,IAAS,EAC1C,CAAC,EAAO,GAAY,EAAsB,CAAI,EA4DpD,OA1DA,MAAgB,CACd,IAAM,EAAU,EAAI,QACpB,GAAI,CAAC,GAAa,CAAC,EAAS,CAC1B,EAAU,GAAc,IAAa,EAAO,EAAW,CAAK,EAC5D,MACF,CAEA,IAAM,EAAa,IAAS,WAItB,EAAQ,iBAAiB,CAAO,EAIhC,EAAW,EAAa,EAAM,UAAY,EAAM,UAChD,EAAY,IAAa,UAAY,IAAa,OAElD,MAAgB,CACpB,IAAM,EAAS,EAAa,EAAQ,UAAY,EAAQ,WAClD,EAAQ,EAAa,EAAQ,aAAe,EAAQ,YACpD,EAAU,EAAa,EAAQ,aAAe,EAAQ,YAGtD,EAAW,CAAC,GAAa,EAAS,EAClC,EAAS,CAAC,GAAa,EAAQ,EAAU,EAAS,EACxD,EAAU,GACR,EAAS,WAAa,GAAY,EAAS,SAAW,EAAS,EAAW,CAAE,WAAU,QAAO,CAC/F,CACF,EAEM,EAAU,IAAI,eAAe,CAAO,EAGpC,MAAuB,CAC3B,EAAQ,WAAW,EACnB,EAAQ,QAAQ,CAAO,EACvB,IAAK,IAAM,KAAS,MAAM,KAAK,EAAQ,QAAQ,EAAG,EAAQ,QAAQ,CAAK,CACzE,EAIM,EAAY,IAAI,qBAAuB,CAC3C,EAAe,EACf,EAAQ,CACV,CAAC,EAMD,OAJA,EAAQ,EACR,EAAe,EACf,EAAU,QAAQ,EAAS,CAAE,UAAW,GAAM,QAAS,GAAM,cAAe,EAAK,CAAC,EAClF,EAAQ,iBAAiB,SAAU,EAAS,CAAE,QAAS,EAAK,CAAC,MAChD,CACX,EAAQ,oBAAoB,SAAU,CAAO,EAC7C,EAAU,WAAW,EACrB,EAAQ,WAAW,CACrB,CACF,EAAG,CAAC,EAAK,EAAM,CAAS,CAAC,EAElB,CACT"}