{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "scroll-area",
  "type": "registry:ui",
  "title": "Scroll Area",
  "description": "Native scrolling with thin overlay bars and edges that fade in as you scroll.",
  "categories": [
    "disclosure"
  ],
  "dependencies": [
    "@base-ui/react"
  ],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json"
  ],
  "files": [
    {
      "path": "components/ui/scroll-area.tsx",
      "content": "\"use client\";\nimport { ScrollArea as BaseScrollArea } from \"@base-ui/react/scroll-area\";\nimport { cn } from \"@/lib/cn\";\n\ntype Axis = \"vertical\" | \"horizontal\" | \"both\";\n\ntype Edge = \"top\" | \"bottom\" | \"left\" | \"right\";\n\n// Edge fades read how far the viewport is from each edge (Base UI writes those as\n// CSS variables on scroll), so a fade grows in over the first 24px of travel\n// instead of switching on. Before the first measure, only the far edges fade.\nconst FADE = \"24px\";\nfunction edgeMask(axis: \"x\" | \"y\", start: boolean, end: boolean) {\n  const dir = axis === \"y\" ? \"to bottom\" : \"to right\";\n  const a = start ? `min(${FADE}, var(--scroll-area-overflow-${axis}-start, 0px))` : \"0px\";\n  const b = end ? `min(${FADE}, var(--scroll-area-overflow-${axis}-end, ${FADE}))` : \"0px\";\n  // Any opaque color works in a mask; only its alpha is read.\n  return `linear-gradient(${dir}, transparent 0, var(--fg) ${a}, var(--fg) calc(100% - ${b}), transparent 100%)`;\n}\n\nexport type ScrollAreaProps = Omit<BaseScrollArea.Root.Props, \"className\" | \"children\"> & {\n  children: React.ReactNode;\n  /** Which way the content overflows. Only the scrollbars you ask for are drawn. */\n  axis?: Axis;\n  /**\n   * Fade the edges where there's more to scroll to. Pass the edges to fade when some\n   * shouldn't, such as a sticky header on top: `fade={[\"bottom\", \"right\"]}`.\n   */\n  fade?: boolean | Edge[];\n  /** Classes for the outer box: give it a height (or max-height) here. */\n  className?: string;\n  /** Classes for the scrolling element itself. */\n  viewportClassName?: string;\n  /** Classes for the content wrapper inside the viewport (padding lives well here). */\n  contentClassName?: string;\n  /** Names the scroll region for screen readers when it holds more than a list or table can say. */\n  \"aria-label\"?: string;\n};\n\n/**\n * A native scroll container, so wheel, touch, keyboard, find-in-page and momentum\n * all behave as the platform does, with thin scrollbars drawn over the content.\n * They show while the pointer is over the area or it's scrolling, then get out of the way.\n */\nexport function ScrollArea({ children, axis = \"vertical\", fade = true, className, viewportClassName, contentClassName, \"aria-label\": label, ...rest }: ScrollAreaProps) {\n  const y = axis !== \"horizontal\";\n  const x = axis !== \"vertical\";\n  const edges = new Set<Edge>(fade === true ? [\"top\", \"bottom\", \"left\", \"right\"] : fade || []);\n  const layers = [\n    y && (edges.has(\"top\") || edges.has(\"bottom\")) && edgeMask(\"y\", edges.has(\"top\"), edges.has(\"bottom\")),\n    x && (edges.has(\"left\") || edges.has(\"right\")) && edgeMask(\"x\", edges.has(\"left\"), edges.has(\"right\")),\n  ].filter(Boolean);\n  const mask = layers.length ? layers.join(\", \") : undefined;\n\n  return (\n    <BaseScrollArea.Root\n      data-axis={axis}\n      className={cn(\n        \"group/scroll relative flex min-h-0 min-w-0 flex-col overflow-hidden\",\n        // The viewport takes focus for keyboard scrolling; the ring belongs to the whole area, drawn just inside it.\n        \"rounded-[inherit] has-[[data-scroll-viewport]:focus-visible]:outline-solid has-[[data-scroll-viewport]:focus-visible]:outline-1 has-[[data-scroll-viewport]:focus-visible]:-outline-offset-1 has-[[data-scroll-viewport]:focus-visible]:outline-fg-3\",\n        className,\n      )}\n      {...rest}\n    >\n      <BaseScrollArea.Viewport\n        data-scroll-viewport=\"\"\n        aria-label={label}\n        role={label ? \"region\" : undefined}\n        style={mask ? { maskImage: mask, WebkitMaskImage: mask, maskComposite: \"intersect\", WebkitMaskComposite: \"source-in\" } : undefined}\n        className={cn(\"min-h-0 flex-1 overscroll-contain rounded-[inherit] outline-none\", viewportClassName)}\n      >\n        <BaseScrollArea.Content className={cn(x ? \"min-w-fit\" : \"min-w-0\", contentClassName)}>{children}</BaseScrollArea.Content>\n      </BaseScrollArea.Viewport>\n      {y && <ScrollBar orientation=\"vertical\" />}\n      {x && <ScrollBar orientation=\"horizontal\" />}\n      {y && x && <BaseScrollArea.Corner className=\"bg-transparent\" />}\n    </BaseScrollArea.Root>\n  );\n}\n\nexport type ScrollBarProps = Omit<BaseScrollArea.Scrollbar.Props, \"className\"> & { className?: string };\n\n/**\n * A 10px lane with a 4px thumb that thickens to 6px under the pointer. Hidden and\n * click-through at rest; it appears at once and fades out on its own.\n */\nexport function ScrollBar({ orientation = \"vertical\", className, ...rest }: ScrollBarProps) {\n  const vertical = orientation === \"vertical\";\n  return (\n    <BaseScrollArea.Scrollbar\n      orientation={orientation}\n      className={cn(\n        \"group/bar z-10 flex touch-none select-none p-0.5\",\n        vertical ? \"w-2.5 justify-center\" : \"h-2.5 flex-col justify-center\",\n        // Leaving: a short beat, then a slow fade. Arriving: at once.\n        \"pointer-events-none opacity-0 transition-opacity delay-150 duration-300 ease-out\",\n        \"data-hovering:pointer-events-auto data-hovering:opacity-100 data-hovering:delay-0 data-hovering:duration-100\",\n        \"data-scrolling:pointer-events-auto data-scrolling:opacity-100 data-scrolling:delay-0 data-scrolling:duration-100\",\n        className,\n      )}\n      {...rest}\n    >\n      <BaseScrollArea.Thumb\n        className={cn(\n          \"relative rounded-full bg-fg/25 duration-150 ease-out\",\n          \"group-hover/bar:bg-fg/40 active:bg-fg/55\",\n          // A generous invisible grip, so a 4px thumb is still easy to catch.\n          \"before:absolute before:-inset-1.5 before:content-['']\",\n          // Only the cross axis animates: the length follows the scroll position and must never lag it.\n          vertical ? \"w-1 transition-[width,background-color] group-hover/bar:w-1.5 active:w-1.5\" : \"h-1 transition-[height,background-color] group-hover/bar:h-1.5 active:h-1.5\",\n        )}\n      />\n    </BaseScrollArea.Scrollbar>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/scroll-area"
}
