{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "scroll-area",
  "title": "Scroll Area",
  "description": "A scroller with a styled bar that matches the theme, as a hover bar, a thin overlay, edge shadows or a snapping row.",
  "dependencies": [
    "radix-ui"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/miseui/ui/scroll-area.tsx",
      "content": "import type * as React from \"react\"\nimport { useRef } from \"react\"\nimport { ScrollArea as ScrollAreaPrimitive } from \"radix-ui\"\nimport {\n  EdgeShadow,\n  FADE_CLASS,\n  Scrollbar,\n  edgeProgress,\n  useScrollEdges,\n  type ScrollAreaVariant,\n  type ScrollAxis,\n} from \"@/registry/miseui/ui/scroll-area-parts\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\ntype ScrollAreaEffect = \"edge-fade\"\ntype ScrollAreaOrientation = \"vertical\" | \"horizontal\" | \"both\"\ntype RootType = NonNullable<React.ComponentProps<typeof ScrollAreaPrimitive.Root>[\"type\"]>\n\nconst DEFAULT_TYPE: Record<ScrollAreaVariant, RootType> = {\n  plain: \"hover\",\n  overlay: \"scroll\",\n  shadow: \"scroll\",\n  snap: \"hover\",\n}\n\ntype ScrollAreaProps = React.ComponentProps<typeof ScrollAreaPrimitive.Root> & {\n  /** `plain` a bar on hover, `overlay` a thin floating bar while scrolling, `shadow` edges that say more is there, `snap` a row that stops on each child. */\n  variant?: ScrollAreaVariant\n  /** `edge-fade` fades content out at the edges through a mask. Removed under reduced transparency. */\n  effect?: ScrollAreaEffect\n  /** Defaults to `horizontal` for `snap` and `vertical` otherwise. */\n  orientation?: ScrollAreaOrientation\n  /** Classes for the scrolling element itself. */\n  viewportClassName?: string\n}\n\nfunction SnapTrack({ axis, children }: { axis: ScrollAxis; children: React.ReactNode }) {\n  return (\n    <div data-part=\"snap-track\" className={cn(\"gap-3 *:snap-start\", axis === \"x\" ? \"flex w-max *:shrink-0\" : \"grid\")}>\n      {children}\n    </div>\n  )\n}\n\nfunction ScrollChrome({ variant, orientation, axis }: { variant: ScrollAreaVariant; orientation: ScrollAreaOrientation; axis: ScrollAxis }) {\n  if (variant === \"snap\") return null\n  return (\n    <>\n      {variant === \"shadow\" ? (\n        <>\n          <EdgeShadow axis={axis} side=\"start\" />\n          <EdgeShadow axis={axis} side=\"end\" />\n        </>\n      ) : null}\n      {orientation !== \"horizontal\" ? <Scrollbar variant={variant} orientation=\"vertical\" /> : null}\n      {orientation !== \"vertical\" ? <Scrollbar variant={variant} orientation=\"horizontal\" /> : null}\n      {orientation === \"both\" ? <ScrollAreaPrimitive.Corner data-slot=\"scroll-area-corner\" /> : null}\n    </>\n  )\n}\n\nfunction viewportClass(variant: ScrollAreaVariant, orientation: ScrollAreaOrientation, axis: ScrollAxis, fade: boolean) {\n  const snapClass = axis === \"x\" ? \"snap-x snap-mandatory overscroll-x-contain\" : \"snap-y snap-mandatory overscroll-y-contain\"\n  return cn(\n    \"size-full rounded-[inherit] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:ring-inset\",\n    // Radix wraps content in display: table, which lets text push wider than the viewport and never wrap.\n    orientation === \"vertical\" && \"[&>div]:block!\",\n    variant === \"snap\" && snapClass,\n    fade && FADE_CLASS[axis],\n    fade && \"[@media(prefers-reduced-transparency:reduce)]:[mask-image:none]\",\n  )\n}\n\nfunction ScrollArea({\n  variant = \"plain\",\n  effect,\n  orientation = variant === \"snap\" ? \"horizontal\" : \"vertical\",\n  type = DEFAULT_TYPE[variant],\n  className,\n  viewportClassName,\n  children,\n  \"aria-label\": ariaLabel,\n  \"aria-labelledby\": ariaLabelledBy,\n  ...props\n}: ScrollAreaProps) {\n  const viewportRef = useRef<HTMLDivElement>(null)\n  const axis: ScrollAxis = orientation === \"horizontal\" ? \"x\" : \"y\"\n  const fade = effect === \"edge-fade\"\n  useScrollEdges(viewportRef, axis, variant === \"shadow\" || fade)\n  return (\n    <ScrollAreaPrimitive.Root\n      data-slot=\"scroll-area\"\n      data-variant={variant}\n      data-effect={effect}\n      data-orientation={orientation}\n      type={type}\n      // Starts at 0 so no cue flashes before the first measure; the measure then overrides these inline.\n      className={cn(\"relative overflow-hidden [--scroll-end:0] [--scroll-start:0]\", className)}\n      {...props}\n    >\n      <ScrollAreaPrimitive.Viewport\n        ref={viewportRef}\n        data-slot=\"scroll-area-viewport\"\n        // A scroller the keyboard cannot reach is a dead end for anyone without a wheel.\n        tabIndex={0}\n        role={ariaLabel !== undefined || ariaLabelledBy !== undefined ? \"region\" : undefined}\n        aria-label={ariaLabel}\n        aria-labelledby={ariaLabelledBy}\n        className={cn(viewportClass(variant, orientation, axis, fade), viewportClassName)}\n      >\n        {variant === \"snap\" ? <SnapTrack axis={axis}>{children}</SnapTrack> : children}\n      </ScrollAreaPrimitive.Viewport>\n      <ScrollChrome variant={variant} orientation={orientation} axis={axis} />\n    </ScrollAreaPrimitive.Root>\n  )\n}\n\nexport { ScrollArea, edgeProgress, type ScrollAreaProps, type ScrollAreaVariant, type ScrollAreaEffect, type ScrollAreaOrientation }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/scroll-area-parts.tsx",
      "content": "import type * as React from \"react\"\nimport { useEffect } from \"react\"\nimport { ScrollArea as ScrollAreaPrimitive } from \"radix-ui\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\ntype ScrollAreaVariant = \"plain\" | \"overlay\" | \"shadow\" | \"snap\"\ntype ScrollAxis = \"x\" | \"y\"\ntype BarOrientation = \"vertical\" | \"horizontal\"\n\n/** How far from an edge the cue reaches full strength, in px. Matches the fade length in the mask below. */\nconst EDGE_RANGE_PX = 24\n\nconst BAR_AXIS_CLASS: Record<BarOrientation, string> = {\n  vertical: \"h-full\",\n  horizontal: \"flex-col\",\n}\n\nconst BAR_SIZE_CLASS: Record<Exclude<ScrollAreaVariant, \"snap\">, Record<BarOrientation, string>> = {\n  plain: { vertical: \"w-3\", horizontal: \"h-3\" },\n  // Thin until the pointer reaches for it, so it floats over the content without covering any.\n  overlay: { vertical: \"w-2 hover:w-3\", horizontal: \"h-2 hover:h-3\" },\n  shadow: { vertical: \"w-2\", horizontal: \"h-2\" },\n}\n\nconst THUMB_CLASS: Record<Exclude<ScrollAreaVariant, \"snap\">, string> = {\n  plain: \"bg-border hover:bg-muted-foreground/50\",\n  overlay: \"bg-foreground/30 hover:bg-foreground/50\",\n  shadow: \"bg-foreground/20 hover:bg-foreground/40\",\n}\n\nconst EDGE_CLASS: Record<ScrollAxis, Record<\"start\" | \"end\", string>> = {\n  y: {\n    start: \"inset-x-0 top-0 h-4 bg-linear-to-b opacity-(--scroll-start)\",\n    end: \"inset-x-0 bottom-0 h-4 bg-linear-to-t opacity-(--scroll-end)\",\n  },\n  x: {\n    start: \"inset-y-0 left-0 w-4 bg-linear-to-r opacity-(--scroll-start)\",\n    end: \"inset-y-0 right-0 w-4 bg-linear-to-l opacity-(--scroll-end)\",\n  },\n}\n\nconst FADE_CLASS: Record<ScrollAxis, string> = {\n  y: \"[mask-image:linear-gradient(to_bottom,transparent,black_calc(var(--scroll-start,0)*24px),black_calc(100%-var(--scroll-end,0)*24px),transparent)]\",\n  x: \"[mask-image:linear-gradient(to_right,transparent,black_calc(var(--scroll-start,0)*24px),black_calc(100%-var(--scroll-end,0)*24px),transparent)]\",\n}\n\n/** 0 at an edge, 1 once there are EDGE_RANGE_PX of content beyond it, so a cue grows with the scroll instead of popping in. */\nfunction edgeProgress(distancePx: number) {\n  return Math.min(1, Math.max(0, distancePx / EDGE_RANGE_PX))\n}\n\nfunction measureEdges(viewport: HTMLElement, axis: ScrollAxis) {\n  // scrollLeft runs negative in right-to-left layouts.\n  const position = Math.abs(axis === \"y\" ? viewport.scrollTop : viewport.scrollLeft)\n  const overflow =\n    axis === \"y\" ? viewport.scrollHeight - viewport.clientHeight : viewport.scrollWidth - viewport.clientWidth\n  return { start: edgeProgress(position), end: edgeProgress(overflow - position) }\n}\n\n/** Writes straight to CSS variables: this runs on every scroll event and must not re-render the content. */\nfunction useScrollEdges(viewportRef: React.RefObject<HTMLDivElement | null>, axis: ScrollAxis, enabled: boolean) {\n  useEffect(() => {\n    const viewport = viewportRef.current\n    const root = viewport?.parentElement\n    if (!enabled || !viewport || !root) return\n    const update = () => {\n      const { start, end } = measureEdges(viewport, axis)\n      root.style.setProperty(\"--scroll-start\", String(start))\n      root.style.setProperty(\"--scroll-end\", String(end))\n    }\n    update()\n    viewport.addEventListener(\"scroll\", update, { passive: true })\n    const observer = new ResizeObserver(update)\n    observer.observe(viewport)\n    if (viewport.firstElementChild) observer.observe(viewport.firstElementChild)\n    return () => {\n      viewport.removeEventListener(\"scroll\", update)\n      observer.disconnect()\n    }\n  }, [viewportRef, axis, enabled])\n}\n\nfunction Scrollbar({ variant, orientation }: { variant: Exclude<ScrollAreaVariant, \"snap\">; orientation: BarOrientation }) {\n  return (\n    <ScrollAreaPrimitive.Scrollbar\n      data-slot=\"scroll-area-scrollbar\"\n      data-variant={variant}\n      orientation={orientation}\n      className={cn(\n        \"flex touch-none p-0.5 select-none\",\n        \"transition-[width,height,opacity] duration-(--duration-fast) ease-(--ease-mise) motion-reduce:transition-none\",\n        \"data-[state=hidden]:animate-out data-[state=hidden]:fade-out-0 data-[state=visible]:animate-in data-[state=visible]:fade-in-0\",\n        BAR_AXIS_CLASS[orientation],\n        BAR_SIZE_CLASS[variant][orientation],\n      )}\n    >\n      <ScrollAreaPrimitive.Thumb\n        data-slot=\"scroll-area-thumb\"\n        className={cn(\n          \"relative flex-1 rounded-full transition-colors duration-(--duration-fast) ease-(--ease-mise)\",\n          // The bar is far thinner than a fingertip; this gives it a 40 px target on touch.\n          \"pointer-coarse:before:absolute pointer-coarse:before:top-1/2 pointer-coarse:before:left-1/2 pointer-coarse:before:size-10 pointer-coarse:before:-translate-1/2\",\n          THUMB_CLASS[variant],\n        )}\n      />\n    </ScrollAreaPrimitive.Scrollbar>\n  )\n}\n\nfunction EdgeShadow({ axis, side }: { axis: ScrollAxis; side: \"start\" | \"end\" }) {\n  return (\n    <div\n      aria-hidden=\"true\"\n      data-part={`edge-${side}`}\n      className={cn(\"pointer-events-none absolute from-foreground/10 to-transparent dark:from-black/50\", EDGE_CLASS[axis][side])}\n    />\n  )\n}\n\nexport { EdgeShadow, FADE_CLASS, Scrollbar, edgeProgress, useScrollEdges, type ScrollAreaVariant, type ScrollAxis }\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "display"
  ],
  "type": "registry:ui"
}