{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "scroll-fade",
  "title": "Scroll Fade",
  "description": "A scroll container with top/bottom token-gradient fades that appear while more content exists in that direction, plus an optional click-to-scroll 'more below' indicator pill — built for long sidebar navs and list panels.",
  "dependencies": [
    "@untitledui/icons@^0.0.22"
  ],
  "registryDependencies": [
    "@nx-ui/utils"
  ],
  "files": [
    {
      "path": "components/blocks/scroll-fade.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronDown } from \"@untitledui/icons\"\n\nimport { cn } from \"@/lib/utils\"\n\n/**\n * ScrollFade — wraps scrollable content and overlays top/bottom gradient fades\n * that appear only while more content exists in that direction, so a long list\n * (a sidebar nav, a picker, an inbox) visibly communicates \"there's more\".\n *\n * It owns a scroll container of its own (filling whatever flex space its parent\n * grants), with a zero-cost sentinel element at each end watched by an\n * IntersectionObserver: when the top sentinel scrolls out of view the top fade\n * appears, likewise for the bottom; both hide when the content fits. The fades\n * derive their colour from a theme token (default: the sidebar surface via\n * `from-sidebar`), so they hold up across brand themes and dark mode — pass\n * the `from-*` utility matching the surface behind the list (`from-background`\n * on a page card, `from-popover` inside a popover).\n *\n * Beyond the fades, an optional `indicator` renders a small \"more below\" pill —\n * a chevron (plus an optional `indicatorLabel`, e.g. \"More\" or a count hint\n * like \"4 more\") floating at the foot of the list while content remains below\n * the fold. Clicking it scrolls down a viewport; it fades out (and leaves the\n * tab order) once the bottom is reached. Reduced motion gets instant scrolling\n * and no transition.\n *\n * Typical app-shell wiring — inside an AppSidebar's content area:\n *\n * ```tsx\n * <AppSidebar header={…} footer={<UserMenu … />}>\n *   <ScrollFade indicator>\n *     <SidebarNav groups={groups} />\n *   </ScrollFade>\n * </AppSidebar>\n * ```\n */\nfunction ScrollFade({\n  className,\n  fadeFrom = \"from-sidebar\",\n  indicator = false,\n  indicatorLabel,\n  children,\n}: {\n  className?: string\n  /** from-* token utility for the fade colour (defaults to the sidebar bg). */\n  fadeFrom?: string\n  /**\n   * Show the \"there's more down here\" pill while content remains below the\n   * fold. Clicking it scrolls down one viewport.\n   */\n  indicator?: boolean\n  /** Optional pill text beside the chevron — e.g. \"More\" or \"4 more\". */\n  indicatorLabel?: React.ReactNode\n  children: React.ReactNode\n}) {\n  const scrollRef = React.useRef<HTMLDivElement>(null)\n  const topRef = React.useRef<HTMLDivElement>(null)\n  const bottomRef = React.useRef<HTMLDivElement>(null)\n  // Start \"at the ends\" so nothing fades until the observer reports otherwise.\n  const [atTop, setAtTop] = React.useState(true)\n  const [atBottom, setAtBottom] = React.useState(true)\n\n  React.useEffect(() => {\n    const root = scrollRef.current\n    const top = topRef.current\n    const bottom = bottomRef.current\n    if (!root || !top || !bottom) return\n\n    const observer = new IntersectionObserver(\n      (entries) => {\n        for (const entry of entries) {\n          if (entry.target === top) setAtTop(entry.isIntersecting)\n          else if (entry.target === bottom) setAtBottom(entry.isIntersecting)\n        }\n      },\n      { root }\n    )\n    observer.observe(top)\n    observer.observe(bottom)\n    return () => observer.disconnect()\n  }, [])\n\n  const scrollTowardsBottom = React.useCallback(() => {\n    const root = scrollRef.current\n    if (!root) return\n    const reduceMotion = window.matchMedia(\n      \"(prefers-reduced-motion: reduce)\"\n    ).matches\n    root.scrollBy({\n      top: Math.max(root.clientHeight * 0.8, 120),\n      behavior: reduceMotion ? \"auto\" : \"smooth\",\n    })\n  }, [])\n\n  return (\n    <div className={cn(\"relative flex min-h-0 flex-1 flex-col\", className)}>\n      <div\n        ref={scrollRef}\n        data-slot=\"scroll-fade-viewport\"\n        // scroll-py matches the fade height, so scrollIntoView({block:\"nearest\"})\n        // lands targets clear of the gradient rather than underneath it.\n        className=\"flex min-h-0 flex-1 flex-col overflow-y-auto overscroll-contain scroll-py-10\"\n      >\n        <div ref={topRef} aria-hidden=\"true\" className=\"h-px w-full shrink-0\" />\n        <div className=\"flex flex-col gap-2\">{children}</div>\n        <div ref={bottomRef} aria-hidden=\"true\" className=\"h-px w-full shrink-0\" />\n      </div>\n      <div\n        aria-hidden=\"true\"\n        data-slot=\"scroll-fade-top\"\n        data-visible={!atTop}\n        className={cn(\n          \"pointer-events-none absolute inset-x-0 top-0 z-10 h-10 bg-linear-to-b to-transparent transition-opacity duration-200 motion-reduce:transition-none\",\n          fadeFrom,\n          atTop ? \"opacity-0\" : \"opacity-100\"\n        )}\n      />\n      <div\n        aria-hidden=\"true\"\n        data-slot=\"scroll-fade-bottom\"\n        data-visible={!atBottom}\n        className={cn(\n          \"pointer-events-none absolute inset-x-0 bottom-0 z-10 h-10 bg-linear-to-t to-transparent transition-opacity duration-200 motion-reduce:transition-none\",\n          fadeFrom,\n          atBottom ? \"opacity-0\" : \"opacity-100\"\n        )}\n      />\n      {indicator ? (\n        <div\n          data-slot=\"scroll-fade-indicator\"\n          data-visible={!atBottom}\n          className={cn(\n            \"pointer-events-none absolute inset-x-0 bottom-1.5 z-20 flex justify-center transition-[opacity,translate] duration-200 ease-out motion-reduce:transition-none\",\n            atBottom ? \"translate-y-1 opacity-0\" : \"translate-y-0 opacity-100\"\n          )}\n        >\n          <button\n            type=\"button\"\n            onClick={scrollTowardsBottom}\n            aria-label=\"Scroll to see more\"\n            aria-hidden={atBottom}\n            tabIndex={atBottom ? -1 : 0}\n            className={cn(\n              \"inline-flex cursor-pointer items-center gap-1 rounded-full border border-border bg-popover text-xs font-medium text-muted-foreground shadow-sm transition-colors outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\",\n              indicatorLabel != null ? \"py-1 pr-2.5 pl-2\" : \"p-1.5\",\n              atBottom ? \"pointer-events-none\" : \"pointer-events-auto\"\n            )}\n          >\n            <ChevronDown className=\"size-3.5 shrink-0\" />\n            {indicatorLabel}\n          </button>\n        </div>\n      ) : null}\n    </div>\n  )\n}\n\nexport { ScrollFade }\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:block"
}