{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"scroll-area","type":"registry:ui","title":"Scroll Area","description":"Native scrolling with slim overlay scrollbars that fade in on hover or scroll, draggable thumbs and soft fading edges where there is more to see.","dependencies":[],"registryDependencies":["utils"],"files":[{"path":"registry/ui/scroll-area.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useRef, type HTMLAttributes, type PointerEvent as ReactPointerEvent } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ntype Axis = \"y\" | \"x\";\n\nexport interface ScrollAreaProps extends HTMLAttributes<HTMLDivElement> {\n  orientation?: \"vertical\" | \"horizontal\" | \"both\";\n  /** When the overlay scrollbars show: on hover or scroll, only while scrolling, or always. */\n  type?: \"hover\" | \"scroll\" | \"always\";\n  /** Fade content out at edges that have more to scroll. */\n  fade?: boolean;\n  /** Fade length in px. */\n  fadeSize?: number;\n  /** Classes for the scrolling viewport (e.g. padding). */\n  viewportClassName?: string;\n}\n\n// Native scrolling underneath (wheel, touch, keyboard, momentum, find-in-page);\n// we hide the platform scrollbar and draw overlay thumbs synced from scroll events.\nexport function ScrollArea({\n  orientation = \"vertical\",\n  type = \"hover\",\n  fade = true,\n  fadeSize = 32,\n  viewportClassName,\n  className,\n  children,\n  ...props\n}: ScrollAreaProps) {\n  const root = useRef<HTMLDivElement>(null);\n  const viewport = useRef<HTMLDivElement>(null);\n  const content = useRef<HTMLDivElement>(null);\n  const thumbs = useRef<Record<Axis, HTMLDivElement | null>>({ y: null, x: null });\n  const tracks = useRef<Record<Axis, HTMLDivElement | null>>({ y: null, x: null });\n  const drag = useRef<{ axis: Axis; start: number; scroll: number } | null>(null);\n  const showY = orientation !== \"horizontal\";\n  const showX = orientation !== \"vertical\";\n\n  useEffect(() => {\n    const r = root.current;\n    const vp = viewport.current;\n    if (!r || !vp) return;\n    let hideTimer = 0;\n\n    const sync = () => {\n      for (const axis of [\"y\", \"x\"] as const) {\n        const thumb = thumbs.current[axis];\n        const track = tracks.current[axis];\n        const pos = axis === \"y\" ? vp.scrollTop : vp.scrollLeft;\n        const size = axis === \"y\" ? vp.scrollHeight : vp.scrollWidth;\n        const view = axis === \"y\" ? vp.clientHeight : vp.clientWidth;\n        const max = size - view;\n        const overflow = max > 1;\n        r.style.setProperty(`--sa-${axis}0`, overflow ? String(Math.min(1, pos / fadeSize)) : \"0\");\n        r.style.setProperty(`--sa-${axis}1`, overflow ? String(Math.min(1, (max - pos) / fadeSize)) : \"0\");\n        if (!thumb || !track) continue;\n        track.dataset.overflow = String(overflow);\n        const trackLen = axis === \"y\" ? track.clientHeight : track.clientWidth;\n        const len = Math.max(24, (trackLen * view) / size);\n        const offset = overflow ? ((trackLen - len) * pos) / max : 0;\n        thumb.style[axis === \"y\" ? \"height\" : \"width\"] = `${len}px`;\n        thumb.style.transform = axis === \"y\" ? `translateY(${offset}px)` : `translateX(${offset}px)`;\n      }\n    };\n\n    const onScroll = () => {\n      sync();\n      r.dataset.scrolling = \"\";\n      clearTimeout(hideTimer);\n      hideTimer = window.setTimeout(() => delete r.dataset.scrolling, 700);\n    };\n\n    sync();\n    vp.addEventListener(\"scroll\", onScroll, { passive: true });\n    const ro = new ResizeObserver(sync);\n    ro.observe(vp);\n    if (content.current) ro.observe(content.current);\n    return () => {\n      vp.removeEventListener(\"scroll\", onScroll);\n      ro.disconnect();\n      clearTimeout(hideTimer);\n    };\n  }, [fadeSize]);\n\n  const metrics = (axis: Axis) => {\n    const vp = viewport.current!;\n    const track = tracks.current[axis]!;\n    const thumb = thumbs.current[axis]!;\n    const trackLen = axis === \"y\" ? track.clientHeight : track.clientWidth;\n    const thumbLen = axis === \"y\" ? thumb.offsetHeight : thumb.offsetWidth;\n    const max = axis === \"y\" ? vp.scrollHeight - vp.clientHeight : vp.scrollWidth - vp.clientWidth;\n    return { vp, track, trackLen, thumbLen, ratio: max / Math.max(1, trackLen - thumbLen) };\n  };\n\n  const onThumbDown = (axis: Axis) => (e: ReactPointerEvent<HTMLDivElement>) => {\n    e.stopPropagation();\n    e.currentTarget.setPointerCapture(e.pointerId);\n    const vp = viewport.current!;\n    drag.current = { axis, start: axis === \"y\" ? e.clientY : e.clientX, scroll: axis === \"y\" ? vp.scrollTop : vp.scrollLeft };\n    root.current!.dataset.dragging = \"\";\n  };\n  const onThumbMove = (e: ReactPointerEvent<HTMLDivElement>) => {\n    const d = drag.current;\n    if (!d) return;\n    const { vp, ratio } = metrics(d.axis);\n    const delta = ((d.axis === \"y\" ? e.clientY : e.clientX) - d.start) * ratio;\n    if (d.axis === \"y\") vp.scrollTop = d.scroll + delta;\n    else vp.scrollLeft = d.scroll + delta;\n  };\n  const onThumbUp = () => {\n    drag.current = null;\n    delete root.current?.dataset.dragging;\n  };\n  // Clicking the track glides the thumb's center to the pointer.\n  const onTrackDown = (axis: Axis) => (e: ReactPointerEvent<HTMLDivElement>) => {\n    const { vp, track, thumbLen, ratio } = metrics(axis);\n    const rect = track.getBoundingClientRect();\n    const at = (axis === \"y\" ? e.clientY - rect.top : e.clientX - rect.left) - thumbLen / 2;\n    vp.scrollTo({ [axis === \"y\" ? \"top\" : \"left\"]: at * ratio, behavior: \"smooth\" });\n  };\n\n  const size = `${fadeSize}px`;\n  const grad = (dir: string, a: Axis) =>\n    `linear-gradient(to ${dir},transparent,#000 calc(var(--sa-${a}0,0)*${size}),#000 calc(100% - var(--sa-${a}1,0)*${size}),transparent)`;\n  const mask = fade ? [showY && grad(\"bottom\", \"y\"), showX && grad(\"right\", \"x\")].filter(Boolean).join(\",\") : undefined;\n\n  const bar = (axis: Axis) => (\n    <div\n      ref={(el) => {\n        tracks.current[axis] = el;\n      }}\n      aria-hidden\n      onPointerDown={onTrackDown(axis)}\n      className={cn(\n        \"group/bar absolute z-10 flex touch-none p-0.5 opacity-0 transition-opacity duration-300 select-none data-[overflow=false]:hidden\",\n        axis === \"y\" ? \"inset-y-0 right-0 w-3 justify-end\" : \"inset-x-0 bottom-0 h-3 flex-col justify-end\",\n        axis === \"y\" && showX && \"bottom-3\",\n        type === \"always\" && \"opacity-100\",\n        type === \"hover\" && \"group-hover/sa:opacity-100\",\n        \"group-data-scrolling/sa:opacity-100 group-data-dragging/sa:opacity-100\",\n      )}\n    >\n      <div\n        ref={(el) => {\n          thumbs.current[axis] = el;\n        }}\n        onPointerDown={onThumbDown(axis)}\n        onPointerMove={onThumbMove}\n        onPointerUp={onThumbUp}\n        onPointerCancel={onThumbUp}\n        className={cn(\n          \"rounded-full bg-foreground/25 transition-[background-color,width,height] duration-200 hover:bg-foreground/45 group-data-dragging/sa:bg-foreground/50\",\n          axis === \"y\" ? \"w-1.5 group-hover/bar:w-2 group-data-dragging/sa:w-2\" : \"h-1.5 group-hover/bar:h-2 group-data-dragging/sa:h-2\",\n        )}\n      />\n    </div>\n  );\n\n  return (\n    <div {...props} ref={root} className={cn(\"group/sa relative overflow-hidden\", className)}>\n      <div\n        ref={viewport}\n        tabIndex={0}\n        className={cn(\n          \"size-full rounded-[inherit] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:ring-inset\",\n          \"[scrollbar-width:none] [&::-webkit-scrollbar]:hidden\",\n          orientation === \"vertical\" ? \"overflow-y-auto overflow-x-hidden\" : orientation === \"horizontal\" ? \"overflow-x-auto overflow-y-hidden\" : \"overflow-auto\",\n          viewportClassName,\n        )}\n        style={mask ? { maskImage: mask, WebkitMaskImage: mask, maskComposite: \"intersect\", WebkitMaskComposite: \"source-in\" } : undefined}\n      >\n        <div ref={content} className={cn(orientation === \"vertical\" ? \"min-w-full\" : \"w-max min-w-full\")}>\n          {children}\n        </div>\n      </div>\n      {showY && bar(\"y\")}\n      {showX && bar(\"x\")}\n    </div>\n  );\n}\n"}]}