{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "scroll-shadow",
  "type": "registry:ui",
  "title": "Scroll Shadow",
  "description": "Shades only the edges with more to scroll, tied to the scroll itself.",
  "categories": [
    "scroll"
  ],
  "dependencies": [
    "@base-ui/react"
  ],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json"
  ],
  "files": [
    {
      "path": "components/ui/scroll-shadow.tsx",
      "content": "\"use client\";\nimport { ScrollArea } from \"@base-ui/react/scroll-area\";\nimport { useId } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\n// Edges appear only where there is more to scroll. Where the browser has\n// scroll-driven animations they are tied to the scroll position itself: the top\n// edge fades in over the first `size` px, the bottom one fades out over the\n// last, with no script and before hydration. Elsewhere the scroll area's own\n// overflow attributes switch them with a short transition.\nconst css = `\n[data-ss-root] { timeline-scope: var(--ss-y), var(--ss-x); }\n[data-ss-viewport] { scroll-timeline: var(--ss-y) y, var(--ss-x) x; }\n\n[data-ss-edge] { position: absolute; pointer-events: none; opacity: 0; transition: opacity 180ms var(--ease-out-quart); }\n[data-ss-edge=\"top\"] { inset: 0 0 auto 0; height: var(--ss-size); background: linear-gradient(to bottom, color-mix(in oklab, var(--ss-color) 90%, transparent), transparent); box-shadow: inset 0 1px 0 var(--line-2); }\n[data-ss-edge=\"bottom\"] { inset: auto 0 0 0; height: var(--ss-size); background: linear-gradient(to top, color-mix(in oklab, var(--ss-color) 90%, transparent), transparent); box-shadow: inset 0 -1px 0 var(--line-2); }\n[data-ss-edge=\"left\"] { inset: 0 auto 0 0; width: var(--ss-size); background: linear-gradient(to right, color-mix(in oklab, var(--ss-color) 90%, transparent), transparent); box-shadow: inset 1px 0 0 var(--line-2); }\n[data-ss-edge=\"right\"] { inset: 0 0 0 auto; width: var(--ss-size); background: linear-gradient(to left, color-mix(in oklab, var(--ss-color) 90%, transparent), transparent); box-shadow: inset -1px 0 0 var(--line-2); }\n[data-ss-root][data-overflow-y-start] > [data-ss-edge=\"top\"],\n[data-ss-root][data-overflow-y-end] > [data-ss-edge=\"bottom\"],\n[data-ss-root][data-overflow-x-start] > [data-ss-edge=\"left\"],\n[data-ss-root][data-overflow-x-end] > [data-ss-edge=\"right\"] { opacity: 1; }\n\n@property --ss-fade-top { syntax: \"<length>\"; inherits: false; initial-value: 0px; }\n@property --ss-fade-bottom { syntax: \"<length>\"; inherits: false; initial-value: 0px; }\n@property --ss-fade-left { syntax: \"<length>\"; inherits: false; initial-value: 0px; }\n@property --ss-fade-right { syntax: \"<length>\"; inherits: false; initial-value: 0px; }\n[data-ss-variant=\"fade\"] > [data-ss-viewport] {\n  --ss-fade-top: min(var(--ss-size), var(--scroll-area-overflow-y-start, 0px));\n  --ss-fade-bottom: min(var(--ss-size), var(--scroll-area-overflow-y-end, 0px));\n  --ss-fade-left: min(var(--ss-size), var(--scroll-area-overflow-x-start, 0px));\n  --ss-fade-right: min(var(--ss-size), var(--scroll-area-overflow-x-end, 0px));\n  -webkit-mask-image: linear-gradient(to bottom, transparent, var(--fg) var(--ss-fade-top), var(--fg) calc(100% - var(--ss-fade-bottom)), transparent), linear-gradient(to right, transparent, var(--fg) var(--ss-fade-left), var(--fg) calc(100% - var(--ss-fade-right)), transparent);\n  mask-image: linear-gradient(to bottom, transparent, var(--fg) var(--ss-fade-top), var(--fg) calc(100% - var(--ss-fade-bottom)), transparent), linear-gradient(to right, transparent, var(--fg) var(--ss-fade-left), var(--fg) calc(100% - var(--ss-fade-right)), transparent);\n  -webkit-mask-composite: source-in;\n  mask-composite: intersect;\n}\n\n@keyframes ss-in { from { opacity: 0; } to { opacity: 1; } }\n@keyframes ss-out { from { opacity: 1; } to { opacity: 0; } }\n@keyframes ss-fade-top { from { --ss-fade-top: 0px; } to { --ss-fade-top: var(--ss-size); } }\n@keyframes ss-fade-bottom { from { --ss-fade-bottom: var(--ss-size); } to { --ss-fade-bottom: 0px; } }\n@keyframes ss-fade-left { from { --ss-fade-left: 0px; } to { --ss-fade-left: var(--ss-size); } }\n@keyframes ss-fade-right { from { --ss-fade-right: var(--ss-size); } to { --ss-fade-right: 0px; } }\n\n@supports (animation-timeline: scroll()) {\n  [data-ss-edge] { transition: none; animation-timing-function: linear; animation-fill-mode: both; animation-duration: auto; }\n  [data-ss-edge=\"top\"] { animation-name: ss-in; animation-timeline: var(--ss-y); animation-range: 0 var(--ss-size); }\n  [data-ss-edge=\"bottom\"] { animation-name: ss-out; animation-timeline: var(--ss-y); animation-range: calc(100% - var(--ss-size)) 100%; }\n  [data-ss-edge=\"left\"] { animation-name: ss-in; animation-timeline: var(--ss-x); animation-range: 0 var(--ss-size); }\n  [data-ss-edge=\"right\"] { animation-name: ss-out; animation-timeline: var(--ss-x); animation-range: calc(100% - var(--ss-size)) 100%; }\n  [data-ss-variant=\"fade\"] > [data-ss-viewport] {\n    animation-name: ss-fade-top, ss-fade-bottom, ss-fade-left, ss-fade-right;\n    animation-timing-function: linear;\n    animation-fill-mode: both;\n    animation-duration: auto;\n    animation-timeline: var(--ss-y), var(--ss-y), var(--ss-x), var(--ss-x);\n    animation-range: 0 var(--ss-size), calc(100% - var(--ss-size)) 100%, 0 var(--ss-size), calc(100% - var(--ss-size)) 100%;\n  }\n  /* These follow the scroll position; they are state, not motion, so reduced motion keeps them. */\n  @media (prefers-reduced-motion: reduce) {\n    [data-ss-root] [data-ss-edge], [data-ss-root][data-ss-variant=\"fade\"] > [data-ss-viewport] { animation-duration: auto !important; }\n  }\n}\n`;\n\nconst edgesFor = { vertical: [\"top\", \"bottom\"], horizontal: [\"left\", \"right\"], both: [\"top\", \"bottom\", \"left\", \"right\"] } as const;\n\nexport type ScrollShadowProps = Omit<ScrollArea.Root.Props, \"children\" | \"className\"> & {\n  children?: React.ReactNode;\n  className?: string;\n  /** Which way it scrolls. */\n  orientation?: \"vertical\" | \"horizontal\" | \"both\";\n  /**\n   * shadow draws a hairline and a soft shade at each edge with more content;\n   * fade masks the content itself out toward those edges, for any background.\n   */\n  variant?: \"shadow\" | \"fade\";\n  /** Depth of the shade or fade, and the scroll distance over which it arrives, in px. */\n  size?: number;\n  /** Show the thin overlay scrollbar while hovering or scrolling. */\n  scrollbar?: boolean;\n  /** Names the scroll region (role=region) for assistive tech. Set it whenever the region can take focus. */\n  label?: string;\n  viewportRef?: React.Ref<HTMLDivElement>;\n  viewportClassName?: string;\n};\n\nexport function ScrollShadow({\n  orientation = \"vertical\",\n  variant = \"shadow\",\n  size = 20,\n  scrollbar = true,\n  label,\n  viewportRef,\n  viewportClassName,\n  className,\n  style,\n  children,\n  ...rest\n}: ScrollShadowProps) {\n  const id = useId().replace(/[^a-zA-Z0-9_-]/g, \"\");\n  const vertical = orientation !== \"horizontal\";\n  const horizontal = orientation !== \"vertical\";\n\n  return (\n    <ScrollArea.Root\n      data-ss-root=\"\"\n      data-ss-variant={variant}\n      data-orientation={orientation}\n      className={cn(\"relative min-h-0 overflow-hidden\", className)}\n      style={{ \"--ss-y\": `--ss-y-${id}`, \"--ss-x\": `--ss-x-${id}`, \"--ss-size\": `${size}px`, \"--ss-color\": \"var(--scroll-shadow-color, var(--page))\", ...(style as React.CSSProperties) } as React.CSSProperties}\n      {...rest}\n    >\n      <style href=\"stealth-scroll-shadow\" precedence=\"default\">\n        {css}\n      </style>\n      <ScrollArea.Viewport\n        ref={viewportRef}\n        data-ss-viewport=\"\"\n        role={label ? \"region\" : undefined}\n        aria-label={label}\n        className={cn(\n          \"size-full overscroll-contain rounded-[inherit] outline-none\",\n          \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:-outline-offset-1 focus-visible:outline-fg-3\",\n          !vertical && \"overscroll-y-auto\",\n          viewportClassName,\n        )}\n      >\n        <ScrollArea.Content\n          className={cn(horizontal && !vertical && \"h-full\")}\n          // Vertical-only content takes the viewport's width, so long rows truncate instead of scrolling sideways.\n          style={horizontal ? undefined : { minWidth: 0 }}\n        >\n          {children}\n        </ScrollArea.Content>\n      </ScrollArea.Viewport>\n\n      {variant === \"shadow\" && edgesFor[orientation].map((edge) => <span key={edge} data-ss-edge={edge} aria-hidden />)}\n\n      {scrollbar && vertical && <Bar orientation=\"vertical\" />}\n      {scrollbar && horizontal && <Bar orientation=\"horizontal\" />}\n      {scrollbar && vertical && horizontal && <ScrollArea.Corner />}\n    </ScrollArea.Root>\n  );\n}\n\n// A thin overlay bar that shows while you hover or scroll, and thickens under the pointer.\nfunction Bar({ orientation }: { orientation: \"vertical\" | \"horizontal\" }) {\n  return (\n    <ScrollArea.Scrollbar\n      orientation={orientation}\n      className={cn(\n        \"group/bar z-1 flex justify-center p-0.5 opacity-0 transition-opacity duration-200 ease-out-quart\",\n        \"pointer-events-none data-hovering:pointer-events-auto data-hovering:opacity-100 data-scrolling:pointer-events-auto data-scrolling:opacity-100 data-scrolling:duration-75\",\n        orientation === \"vertical\" ? \"w-2.5\" : \"h-2.5 flex-col\",\n      )}\n    >\n      <ScrollArea.Thumb\n        className={cn(\n          \"rounded-full bg-fg-4 transition-[background-color,width,height] duration-150 group-hover/bar:bg-fg-3 active:bg-fg-2\",\n          orientation === \"vertical\" ? \"w-1 group-hover/bar:w-1.5\" : \"h-1 group-hover/bar:h-1.5\",\n        )}\n      />\n    </ScrollArea.Scrollbar>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/scroll-shadow"
}
