{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "scroll-area",
  "title": "Scroll-area",
  "description": "A scroll-area component.",
  "files": [
    {
      "path": "registry/default/scroll-area/scroll-area.tsx",
      "content": "import * as React from \"react\";\nimport { ScrollArea as BaseScrollArea } from \"@base-ui/react/scroll-area\";\n\nimport { cn } from \"@/lib/utils\";\n\nimport \"./scroll-area.css\";\n\ntype FadeEdge = \"top\" | \"bottom\" | \"left\" | \"right\" | \"x\" | \"y\";\ntype FadeEdges = boolean | FadeEdge | FadeEdge[];\n\nfunction parseFadeEdges(fadeEdges: FadeEdges): {\n  top: boolean;\n  bottom: boolean;\n  left: boolean;\n  right: boolean;\n} {\n  if (fadeEdges === true) {\n    return { top: true, bottom: true, left: true, right: true };\n  }\n  if (fadeEdges === false) {\n    return { top: false, bottom: false, left: false, right: false };\n  }\n\n  const edges = Array.isArray(fadeEdges) ? fadeEdges : [fadeEdges];\n  const result = { top: false, bottom: false, left: false, right: false };\n\n  for (const edge of edges) {\n    if (edge === \"x\") {\n      result.left = true;\n      result.right = true;\n    } else if (edge === \"y\") {\n      result.top = true;\n      result.bottom = true;\n    } else {\n      result[edge] = true;\n    }\n  }\n\n  return result;\n}\n\ntype OverscrollBehavior = \"auto\" | \"contain\" | \"none\";\n\ntype ContentMinWidth = \"viewport\" | \"fit-content\";\n\ntype ContentHeight = \"auto\" | \"fill\";\n\ntype ScrollAreaProps = BaseScrollArea.Root.Props & {\n  fadeEdges?: FadeEdges;\n  scrollbarGutter?: boolean;\n  persistScrollbar?: boolean;\n  hideScrollbar?: boolean;\n  nativeScroll?: boolean;\n  overscrollBehavior?: OverscrollBehavior;\n  /**\n   * How wide the content wrapper is allowed to get. `\"viewport\"` keeps it at\n   * the visible width; `\"fit-content\"` restores Base UI's default of growing\n   * to the widest child. See the note on the `Content` element below.\n   */\n  contentMinWidth?: ContentMinWidth;\n  /**\n   * How tall the content wrapper is allowed to get. `\"auto\"` tracks the\n   * children; `\"fill\"` stretches it to at least the viewport height so a flex\n   * child can push a footer down with `mt-auto`. Children of a `\"fill\"` wrapper\n   * size themselves with `flex-1`, not `h-full` — see the note on the `Content`\n   * element below.\n   */\n  contentHeight?: ContentHeight;\n  /** Ref callback for the scrollable viewport element. Useful for virtualization. */\n  viewportRef?: (element: HTMLDivElement | null) => void;\n  /** Additional className for the viewport element */\n  viewportClassName?: string;\n  /**\n   * Additional className for the content wrapper element. Needed when a child\n   * relies on a percentage height against its parent (e.g. Base UI's Select\n   * list gets `max-height: 100%` with `alignItemWithTrigger`), which only\n   * resolves if the wrapper has a definite height like `h-full`. Ignored with\n   * `nativeScroll` (no content wrapper exists there).\n   */\n  contentClassName?: string;\n};\n\n// Root props with no plain-div meaning, dropped on the `nativeScroll` path.\nconst NATIVE_DROPPED_PROPS = [\n  \"render\",\n  \"overflowEdgeThreshold\",\n  \"style\",\n  \"onScroll\",\n  \"onScrollCapture\",\n  \"onWheel\",\n  \"onWheelCapture\",\n] as const;\n\nfunction ScrollArea({\n  className,\n  children,\n  fadeEdges = false,\n  scrollbarGutter = false,\n  persistScrollbar = false,\n  hideScrollbar = false,\n  nativeScroll = false,\n  overscrollBehavior = \"contain\",\n  contentMinWidth = \"viewport\",\n  contentHeight = \"auto\",\n  viewportRef,\n  viewportClassName,\n  contentClassName,\n  ...props\n}: ScrollAreaProps) {\n  if (process.env.NODE_ENV !== \"production\") {\n    if (persistScrollbar && hideScrollbar) {\n      console.error(\n        \"ScrollArea: `persistScrollbar` and `hideScrollbar` cannot be used together.\",\n      );\n    }\n    if (nativeScroll && persistScrollbar) {\n      console.error(\n        \"ScrollArea: `persistScrollbar` is not supported with `nativeScroll`.\",\n      );\n    }\n    if (nativeScroll && contentClassName) {\n      console.error(\n        \"ScrollArea: `contentClassName` is ignored with `nativeScroll` (no content wrapper exists there).\",\n      );\n    }\n  }\n\n  if (nativeScroll) {\n    // Base UI className can be a function; NativeScrollArea only accepts string.\n    const nativeClassName =\n      typeof className === \"string\" ? className : undefined;\n    // Everything else is a plain div prop and passes through (data attributes,\n    // aria, `tabIndex`, handlers), except the Base UI-only ones: `render`,\n    // `overflowEdgeThreshold`, a state-function `style`, and the scroll/wheel\n    // handlers NativeScrollArea doesn't take.\n    const divProps: Record<string, unknown> = { ...props };\n    for (const key of NATIVE_DROPPED_PROPS) delete divProps[key];\n    return (\n      <NativeScrollArea\n        {...(divProps as React.ComponentProps<typeof NativeScrollArea>)}\n        ref={viewportRef}\n        style={typeof props.style === \"function\" ? undefined : props.style}\n        className={nativeClassName}\n        viewportClassName={viewportClassName}\n        fadeEdges={fadeEdges}\n        hideScrollbar={hideScrollbar}\n        scrollbarGutter={scrollbarGutter}\n        overscrollBehavior={overscrollBehavior}\n      >\n        {children}\n      </NativeScrollArea>\n    );\n  }\n\n  const fade = parseFadeEdges(fadeEdges);\n  const hasFade = fade.top || fade.bottom || fade.left || fade.right;\n\n  return (\n    <BaseScrollArea.Root\n      data-slot=\"scroll-area\"\n      className={cn(\"flex size-full min-h-0 flex-col\", className)}\n      {...props}\n    >\n      <BaseScrollArea.Viewport\n        ref={viewportRef}\n        data-slot=\"scroll-area-viewport\"\n        className={cn(\n          \"h-full rounded-[inherit]\",\n          \"focus-visible:outline-ring/50 outline-0 outline-offset-0 outline-transparent transition-[outline-width,outline-offset,outline-color] duration-100 ease-out outline-solid focus-visible:outline-2 focus-visible:outline-offset-2\",\n          hasFade && \"[--scroll-fade-size:min(12%,2.5rem)]\",\n          fade.top &&\n            \"mask-t-from-[calc(100%-min(var(--scroll-fade-size),var(--scroll-area-overflow-y-start,0px)))]\",\n          fade.bottom &&\n            \"mask-b-from-[calc(100%-min(var(--scroll-fade-size),var(--scroll-area-overflow-y-end,var(--scroll-fade-size))))]\",\n          fade.left &&\n            \"mask-l-from-[calc(100%-min(var(--scroll-fade-size),var(--scroll-area-overflow-x-start,0px)))]\",\n          fade.right &&\n            \"mask-r-from-[calc(100%-min(var(--scroll-fade-size),var(--scroll-area-overflow-x-end,var(--scroll-fade-size))))]\",\n          scrollbarGutter &&\n            \"data-has-overflow-x:pb-2.5 data-has-overflow-y:pe-2.5\",\n          overscrollBehavior === \"contain\" && \"overscroll-contain\",\n          overscrollBehavior === \"none\" && \"overscroll-none\",\n          viewportClassName,\n        )}\n      >\n        <BaseScrollArea.Content\n          data-slot=\"scroll-area-content\"\n          // `\"fill\"` stretches the wrapper to the viewport so a short flex\n          // child still has slack for `mt-auto` to push a footer against the\n          // bottom. It uses `min-h-full` rather than `h-full` deliberately:\n          // `h-full` pins the wrapper to exactly the viewport height, which\n          // turns a flex-column child into a fixed-height one, and its items\n          // shrink to fit instead of overflowing — tall content silently\n          // compresses and the scrollbar never appears. `min-h-full` keeps the\n          // wrapper growable, so it fills when content is short and grows when\n          // content is long. The flex column here is what lets children claim\n          // that height with `flex-1` (`h-full` resolves against an auto height\n          // and does nothing).\n          className={cn(\n            contentHeight === \"fill\" && \"flex min-h-full flex-col\",\n            contentClassName,\n          )}\n          // Base UI ships `min-width: fit-content` here. That turns on an\n          // intrinsic sizing pass, and anything un-shrinkable in the subtree\n          // (a `whitespace-pre` code block, a fixed-width table) reports its\n          // full width upward — even from inside a nested scroll container,\n          // which does not insulate its parent. The wrapper then outgrows the\n          // viewport, so a vertical panel sprouts a horizontal scrollbar and\n          // the nested scroller, stretched to that width, never reaches its\n          // own overflow and never scrolls itself.\n          //\n          // `\"viewport\"` skips that pass entirely: width stays `auto`, which\n          // is how this component behaved before the Content wrapper existed.\n          // Overflowing children still extend the scroll range, so sideways\n          // scrolling is intact wherever the child sizes itself. What it gives\n          // up is the wrapper spanning the scroll range, so trailing padding\n          // on a child lands at the fold rather than the far end. Opt back in\n          // with `\"fit-content\"` if you need that.\n          //\n          // Written through `style` because Base UI's own value is inline; a\n          // class would have to fight it with `!important`.\n          style={contentMinWidth === \"viewport\" ? { minWidth: 0 } : undefined}\n        >\n          {children}\n        </BaseScrollArea.Content>\n      </BaseScrollArea.Viewport>\n      {!hideScrollbar && (\n        <>\n          <ScrollBar orientation=\"vertical\" persist={persistScrollbar} />\n          <ScrollBar orientation=\"horizontal\" persist={persistScrollbar} />\n          <BaseScrollArea.Corner />\n        </>\n      )}\n    </BaseScrollArea.Root>\n  );\n}\n\nfunction ScrollBar({\n  className,\n  persist = false,\n  ...props\n}: BaseScrollArea.Scrollbar.Props & {\n  persist?: boolean;\n}) {\n  return (\n    <BaseScrollArea.Scrollbar\n      data-slot=\"scroll-area-scrollbar\"\n      className={cn(\n        \"m-1 flex select-none\",\n        !persist &&\n          \"opacity-0 transition-opacity delay-200 data-hovering:opacity-100 data-hovering:delay-0 data-hovering:duration-100 data-scrolling:opacity-100 data-scrolling:delay-0 data-scrolling:duration-100\",\n        \"data-[orientation=vertical]:w-1.5\",\n        \"data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:flex-col\",\n        className,\n      )}\n      {...props}\n    >\n      <BaseScrollArea.Thumb\n        data-slot=\"scroll-area-thumb\"\n        className=\"bg-scrollbar/50 hover:bg-scrollbar data-scrolling:bg-scrollbar relative flex-1 rounded-full transition-colors\"\n      />\n    </BaseScrollArea.Scrollbar>\n  );\n}\n\n/**\n * Native scroll variant. Fade edges use CSS scroll-driven animations\n * (Chrome 115+); static fallback elsewhere.\n */\nfunction NativeScrollArea({\n  className,\n  viewportClassName,\n  children,\n  fadeEdges = false,\n  hideScrollbar = false,\n  scrollbarGutter = false,\n  overscrollBehavior,\n  ref,\n  style,\n  ...props\n}: Omit<\n  React.ComponentProps<\"div\">,\n  \"onScroll\" | \"onScrollCapture\" | \"onWheel\" | \"onWheelCapture\"\n> & {\n  fadeEdges?: FadeEdges;\n  hideScrollbar?: boolean;\n  scrollbarGutter?: boolean;\n  overscrollBehavior?: OverscrollBehavior;\n  viewportClassName?: string;\n  ref?: (element: HTMLDivElement | null) => void;\n}) {\n  const fade = parseFadeEdges(fadeEdges);\n  const hasFade = fade.top || fade.bottom || fade.left || fade.right;\n\n  // Fixed reveal distance, clamped to the scroll range so short scrollers\n  // still reach a full fade instead of only partially revealing it.\n  const reveal = \"min(var(--scroll-fade-reveal,6rem),100%)\";\n  const fadeAnimations = [\n    fade.top && {\n      name: \"scroll-fade-reveal-top\",\n      timeline: \"scroll(y self)\",\n      range: `0 ${reveal}`,\n    },\n    fade.bottom && {\n      name: \"scroll-fade-reveal-bottom\",\n      timeline: \"scroll(y self)\",\n      range: `calc(100% - ${reveal}) 100%`,\n    },\n    fade.left && {\n      name: \"scroll-fade-reveal-left\",\n      timeline: \"scroll(x self)\",\n      range: `0 ${reveal}`,\n    },\n    fade.right && {\n      name: \"scroll-fade-reveal-right\",\n      timeline: \"scroll(x self)\",\n      range: `calc(100% - ${reveal}) 100%`,\n    },\n  ].filter(Boolean) as { name: string; timeline: string; range: string }[];\n\n  const animationStyle: React.CSSProperties | undefined = hasFade\n    ? {\n        animationName: fadeAnimations.map((a) => a.name).join(\", \"),\n        animationTimeline: fadeAnimations.map((a) => a.timeline).join(\", \"),\n        animationRange: fadeAnimations.map((a) => a.range).join(\", \"),\n        animationTimingFunction: \"ease-in-out\",\n        animationDuration: \"1ms\",\n        animationFillMode: \"both\",\n      }\n    : undefined;\n\n  return (\n    <div\n      ref={ref}\n      data-slot=\"scroll-area\"\n      data-native-scroll\n      className={cn(\n        \"size-full min-h-0 overflow-auto rounded-[inherit]\",\n        \"focus-visible:outline-ring/50 outline-0 outline-offset-0 outline-transparent transition-[outline-width,outline-offset,outline-color] duration-100 ease-out outline-solid focus-visible:outline-2 focus-visible:outline-offset-2\",\n        hasFade && \"[--scroll-fade-size:min(12%,2.5rem)]\",\n        // Mask amounts come from CSS variables driven by scroll-driven\n        // animations where supported, and from a static fallback otherwise.\n        fade.top && \"mask-t-from-[calc(100%-var(--scroll-fade-top))]\",\n        fade.bottom && \"mask-b-from-[calc(100%-var(--scroll-fade-bottom))]\",\n        fade.left && \"mask-l-from-[calc(100%-var(--scroll-fade-left))]\",\n        fade.right && \"mask-r-from-[calc(100%-var(--scroll-fade-right))]\",\n        hideScrollbar ? \"[scrollbar-width:none]\" : \"[scrollbar-width:thin]\",\n        scrollbarGutter && \"[scrollbar-gutter:stable]\",\n        \"[scrollbar-color:var(--color-scrollbar)_transparent]\",\n        overscrollBehavior === \"contain\" && \"overscroll-contain\",\n        overscrollBehavior === \"none\" && \"overscroll-none\",\n        className,\n        viewportClassName,\n      )}\n      style={{ ...animationStyle, ...style }}\n      tabIndex={0}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport { ScrollArea };\nexport type {\n  ScrollAreaProps,\n  FadeEdges,\n  FadeEdge,\n  OverscrollBehavior,\n  ContentMinWidth,\n  ContentHeight,\n};\n",
      "type": "registry:ui",
      "target": "components/ui/cubby-ui/scroll-area/scroll-area.tsx"
    },
    {
      "path": "registry/default/scroll-area/scroll-area.css",
      "content": "/* -------------------------------------------------------------------------------------------------\n * Scroll-Driven Animations for Native Scroll Fade Edges (Progressive Enhancement)\n * Chrome 115+, Safari 26+ (future), Firefox flag-only\n * Fallback: Static fade on both ends of each configured edge\n * -------------------------------------------------------------------------------------------------*/\n\n/* Animatable custom properties for fade amounts (length-percentage so the\n * fade size can be a percentage of the container, e.g. min(12%, 2.5rem)) */\n@property --scroll-fade-top {\n  syntax: \"<length-percentage>\";\n  inherits: false;\n  initial-value: 0px;\n}\n\n@property --scroll-fade-bottom {\n  syntax: \"<length-percentage>\";\n  inherits: false;\n  initial-value: 0px;\n}\n\n@property --scroll-fade-left {\n  syntax: \"<length-percentage>\";\n  inherits: false;\n  initial-value: 0px;\n}\n\n@property --scroll-fade-right {\n  syntax: \"<length-percentage>\";\n  inherits: false;\n  initial-value: 0px;\n}\n\n/**\n * Per-edge fade reveal animations.\n *\n * Each edge animates a single fade amount between 0 and --scroll-fade-size.\n * The scroll distance over which the fade eases in/out is driven by\n * animation-range (set on the element), not by keyframe percentages, so the\n * reveal is a fixed pixel distance independent of content length.\n */\n@keyframes scroll-fade-reveal-top {\n  from {\n    --scroll-fade-top: 0px;\n  }\n  to {\n    --scroll-fade-top: var(--scroll-fade-size);\n  }\n}\n\n@keyframes scroll-fade-reveal-bottom {\n  from {\n    --scroll-fade-bottom: var(--scroll-fade-size);\n  }\n  to {\n    --scroll-fade-bottom: 0px;\n  }\n}\n\n@keyframes scroll-fade-reveal-left {\n  from {\n    --scroll-fade-left: 0px;\n  }\n  to {\n    --scroll-fade-left: var(--scroll-fade-size);\n  }\n}\n\n@keyframes scroll-fade-reveal-right {\n  from {\n    --scroll-fade-right: var(--scroll-fade-size);\n  }\n  to {\n    --scroll-fade-right: 0px;\n  }\n}\n\n/**\n * Fallback for browsers without scroll-driven animations: a static fade on\n * each configured edge. Without scroll awareness both ends stay faded.\n */\n@supports not (animation-timeline: scroll()) {\n  [data-native-scroll] {\n    --scroll-fade-top: var(--scroll-fade-size);\n    --scroll-fade-bottom: var(--scroll-fade-size);\n    --scroll-fade-left: var(--scroll-fade-size);\n    --scroll-fade-right: var(--scroll-fade-size);\n  }\n}\n",
      "type": "registry:file",
      "target": "components/ui/cubby-ui/scroll-area/scroll-area.css"
    }
  ],
  "type": "registry:ui"
}