{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "scroll-area",
  "type": "registry:ui",
  "title": "Scroll Area",
  "description": "An accessible React scroll area for Next.js and Tailwind CSS: native scrolling with a slim scrollbar and soft fades at the edges, built on Radix.",
  "author": "FewTone <https://fewtone.com>",
  "dependencies": [
    "radix-ui@^1.7.0"
  ],
  "registryDependencies": [
    "https://ui.fewtone.com/r/tokens.json"
  ],
  "files": [
    {
      "path": "registry/fewtone/ui/scroll-area.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { ScrollArea as ScrollAreaPrimitive } from \"radix-ui\";\nimport { type ComponentProps, useCallback, useEffect, useLayoutEffect, useRef, useState } from \"react\";\n\n/*\n * Scroll Area (owner's roadmap, 2026-10-16: \"Scrolling with soft fades at the edges\"), on Radix Scroll Area, with\n * shadcn's parts: ScrollArea and ScrollBar.\n * - Native scrolling (wheel, touch, keyboard), with a slim scrollbar laid over the content in place of the\n *   browser's. It fades in while the pointer is over the area and out after (Radix's \"hover\" type).\n * - The thumb is a thin line that thickens when you point at the bar or drag it, so it's easy to grab. It grows\n *   from the area's edge with a scale (a transform), never a width change.\n * - Soft fades at the edges that can scroll, as in Select's list: one per side, shown only while there's more\n *   that way. They're overlays outside the scroller (inside, they'd scroll with the content and add to its size)\n *   and take the area's own background, so on a card or a popover give it that colour (`bg-card`,\n *   `bg-popover`). Scroll-edge fades are a plain fade (ADR 0011), and the first paint shows them without fading.\n * - Named with aria-label or aria-labelledby, the scroller becomes a focusable region, so keyboard users can reach\n *   it and scroll even when nothing inside can take focus.\n */\n\ntype Edges = { top: boolean; bottom: boolean; left: boolean; right: boolean };\n\n/** Which sides have more to scroll to. In right-to-left, scrollLeft runs from 0 (the right end) to negative. */\nfunction edgesOf(node: HTMLElement): Edges {\n  const { scrollTop, scrollLeft, scrollHeight, scrollWidth, clientHeight, clientWidth } = node;\n  const wide = scrollWidth - clientWidth;\n  const fromLeft = node.closest(\"[dir]\")?.getAttribute(\"dir\") === \"rtl\" ? wide + scrollLeft : scrollLeft;\n  return {\n    top: scrollTop > 1,\n    bottom: scrollTop < scrollHeight - clientHeight - 1,\n    left: fromLeft > 1,\n    right: fromLeft < wide - 1,\n  };\n}\n\nconst sameEdges = (a: Edges, b: Edges) =>\n  a.top === b.top && a.bottom === b.bottom && a.left === b.left && a.right === b.right;\n\n/** Each fade: where it lies, and which way it thins out (a static mask over the area's own colour) */\nconst fades = {\n  top: \"inset-x-0 top-0 h-8 mask-b-from-0%\",\n  bottom: \"inset-x-0 bottom-0 h-8 mask-t-from-0%\",\n  left: \"inset-y-0 left-0 w-8 mask-r-from-0%\",\n  right: \"inset-y-0 right-0 w-8 mask-l-from-0%\",\n} as const;\n\nexport type ScrollAreaProps = ComponentProps<typeof ScrollAreaPrimitive.Root>;\n\n/**\n * A scrolling box with a slim scrollbar and soft fades at the edges that can scroll. Give it a height (or a width\n * for sideways scrolling, with a horizontal ScrollBar inside) and a name with aria-label.\n */\nfunction ScrollArea({\n  className,\n  children,\n  \"aria-label\": label,\n  \"aria-labelledby\": labelledBy,\n  ...props\n}: ScrollAreaProps) {\n  const viewport = useRef<HTMLDivElement>(null);\n  const [edges, setEdges] = useState<Edges>({ top: false, bottom: false, left: false, right: false });\n  // The fades fade only once the first measure has shown them, so nothing animates on first paint\n  const [settled, setSettled] = useState(false);\n  const named = label !== undefined || labelledBy !== undefined;\n\n  const update = useCallback(() => {\n    const node = viewport.current;\n    if (!node) return;\n    const next = edgesOf(node);\n    setEdges((current) => (sameEdges(current, next) ? current : next));\n  }, []);\n\n  // Measure before the first paint, and again whenever the area or its content changes size\n  useLayoutEffect(() => {\n    const node = viewport.current;\n    if (!node) return;\n    update();\n    const observer = new ResizeObserver(update);\n    observer.observe(node);\n    if (node.firstElementChild) observer.observe(node.firstElementChild);\n    return () => observer.disconnect();\n  }, [update]);\n\n  useEffect(() => {\n    const frame = requestAnimationFrame(() => setSettled(true));\n    return () => cancelAnimationFrame(frame);\n  }, []);\n\n  return (\n    <ScrollAreaPrimitive.Root\n      data-slot=\"scroll-area\"\n      className={cn(\"relative isolate overflow-hidden bg-background\", className)}\n      {...props}\n    >\n      <ScrollAreaPrimitive.Viewport\n        ref={viewport}\n        data-slot=\"scroll-area-viewport\"\n        onScroll={update}\n        {...(named\n          ? { role: \"region\", \"aria-label\": label, \"aria-labelledby\": labelledBy, tabIndex: 0 }\n          : {})}\n        // The focus ring sits inside the edge, where the area's rounded corners don't clip it\n        className=\"size-full outline-ring focus-visible:outline-(length:--focus-ring-width) focus-visible:-outline-offset-(--focus-ring-width)\"\n        style={{ borderRadius: \"inherit\" }}\n      >\n        {children}\n      </ScrollAreaPrimitive.Viewport>\n      {(Object.keys(fades) as (keyof Edges)[]).map((side) => (\n        <div\n          key={side}\n          aria-hidden\n          data-slot=\"scroll-area-fade\"\n          data-side={side}\n          data-state={edges[side] ? \"visible\" : \"hidden\"}\n          className={cn(\n            \"pointer-events-none absolute bg-inherit data-[state=hidden]:opacity-0\",\n            settled && \"transition-opacity duration-(--duration-fast) ease-out\",\n            fades[side],\n          )}\n        />\n      ))}\n      <ScrollBar />\n      <ScrollAreaPrimitive.Corner data-slot=\"scroll-area-corner\" />\n    </ScrollAreaPrimitive.Root>\n  );\n}\n\n/**\n * The scrollbar. ScrollArea has a vertical one; add `<ScrollBar orientation=\"horizontal\" />` inside it to scroll\n * sideways. It stays mounted, so it can fade in and out.\n */\nfunction ScrollBar({\n  className,\n  orientation = \"vertical\",\n  ...props\n}: ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {\n  const vertical = orientation === \"vertical\";\n  return (\n    <ScrollAreaPrimitive.ScrollAreaScrollbar\n      data-slot=\"scroll-area-scrollbar\"\n      orientation={orientation}\n      forceMount\n      className={cn(\n        // Above the fades, so its ends never fade with the content\n        \"group/scrollbar z-10 flex touch-none p-px select-none\",\n        \"transition-opacity duration-(--duration-base) ease-out data-[state=hidden]:pointer-events-none data-[state=hidden]:opacity-0\",\n        vertical ? \"h-full w-2.5\" : \"h-2.5 flex-col\",\n        className,\n      )}\n      {...props}\n    >\n      {/* Radix moves the thumb with a transform, so the line we see is its ::before, which scales on its own:\n          half its width at rest, at the area's edge, and the full 8px while you point at the bar or drag it */}\n      <ScrollAreaPrimitive.ScrollAreaThumb\n        data-slot=\"scroll-area-thumb\"\n        className={cn(\n          \"relative flex-1 rounded-full\",\n          \"before:absolute before:inset-0 before:rounded-full before:bg-border before:transition before:duration-(--duration-fast) before:ease-out\",\n          \"group-hover/scrollbar:before:bg-muted-foreground/50 group-active/scrollbar:before:bg-muted-foreground/50\",\n          vertical\n            ? \"before:origin-right before:scale-x-50 group-hover/scrollbar:before:scale-x-100 group-active/scrollbar:before:scale-x-100 rtl:before:origin-left\"\n            : \"before:origin-bottom before:scale-y-50 group-hover/scrollbar:before:scale-y-100 group-active/scrollbar:before:scale-y-100\",\n        )}\n      />\n    </ScrollAreaPrimitive.ScrollAreaScrollbar>\n  );\n}\n\nexport { ScrollArea, ScrollBar };\n"
    }
  ],
  "categories": [
    "display"
  ],
  "docs": "Usage and examples: https://ui.fewtone.com/components/scroll-area"
}
