{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sidebar",
  "title": "Sidebar",
  "description": "Collapsible sidebar navigation layout.",
  "dependencies": [
    "@vllnt/ui@^0.3.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/sidebar/sidebar.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState, useSyncExternalStore } from \"react\";\n\nimport { ChevronDown, ChevronLeft, ChevronRight } from \"lucide-react\";\nimport Link from \"next/link\";\nimport { usePathname } from \"next/navigation\";\n\nimport { useMounted } from \"@vllnt/ui\";\nimport { cn } from \"@vllnt/ui\";\nimport { useSidebar } from \"@vllnt/ui\";\n\nexport type SidebarItem = {\n  href: string;\n  title: string;\n};\n\nexport type SidebarSection = {\n  collapsible?: boolean;\n  defaultOpen?: boolean;\n  family?: boolean;\n  href?: string;\n  items: SidebarItem[];\n  title?: string;\n};\n\ntype SidebarProps = {\n  sections: SidebarSection[];\n};\n\nconst getMobileSnapshot = () =>\n  typeof window === \"undefined\" ? false : window.innerWidth < 1024;\n\nconst getServerMobileSnapshot = () => false;\n\nconst subscribeToViewportResize = (onStoreChange: () => void) => {\n  window.addEventListener(\"resize\", onStoreChange);\n\n  return () => {\n    window.removeEventListener(\"resize\", onStoreChange);\n  };\n};\n\nfunction useMobile(setOpen: (open: boolean) => void) {\n  const isMobile = useSyncExternalStore(\n    subscribeToViewportResize,\n    getMobileSnapshot,\n    getServerMobileSnapshot,\n  );\n\n  useEffect(() => {\n    setOpen(!isMobile);\n  }, [isMobile, setOpen]);\n\n  return isMobile;\n}\n\nfunction useScrollFade(\n  containerReference: React.RefObject<HTMLDivElement | null>,\n) {\n  const [showTopFade, setShowTopFade] = useState(false);\n  const [showBottomFade, setShowBottomFade] = useState(false);\n\n  useEffect(() => {\n    const container = containerReference.current;\n    if (!container) return;\n\n    const checkScroll = () => {\n      const { clientHeight, scrollHeight, scrollTop } = container;\n      setShowTopFade(scrollTop > 0);\n      setShowBottomFade(scrollTop < scrollHeight - clientHeight - 1);\n    };\n\n    checkScroll();\n    container.addEventListener(\"scroll\", checkScroll, { passive: true });\n    return () => {\n      container.removeEventListener(\"scroll\", checkScroll);\n    };\n  }, [containerReference]);\n\n  return { showBottomFade, showTopFade };\n}\n\ntype CollapsibleSectionProps = {\n  children: React.ReactNode;\n  collapsible?: boolean;\n  defaultOpen?: boolean;\n  title: string;\n};\n\nfunction CollapsibleSection({\n  children,\n  collapsible = false,\n  defaultOpen = true,\n  title,\n}: CollapsibleSectionProps) {\n  const [isOpen, setIsOpen] = useState(defaultOpen);\n\n  if (!collapsible) {\n    return (\n      <>\n        <div className=\"px-3 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wider\">\n          {title}\n        </div>\n        {children}\n      </>\n    );\n  }\n\n  return (\n    <>\n      <button\n        className=\"flex w-full items-center justify-between px-3 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wider hover:text-foreground transition-colors\"\n        onClick={() => {\n          setIsOpen(!isOpen);\n        }}\n        type=\"button\"\n      >\n        <span>{title}</span>\n        <ChevronDown\n          className={cn(\n            \"size-3 transition-transform duration-200\",\n            isOpen && \"rotate-180\",\n          )}\n        />\n      </button>\n      <div\n        className={cn(\n          \"grid transition-all duration-200 ease-in-out\",\n          isOpen ? \"grid-rows-[1fr] opacity-100\" : \"grid-rows-[0fr] opacity-0\",\n        )}\n        hidden={!isOpen}\n      >\n        <div className=\"overflow-hidden\">{children}</div>\n      </div>\n    </>\n  );\n}\n\ntype FamilyNavProps = {\n  isMobile: boolean;\n  onNavigate: () => void;\n  pathname: string;\n  sections: SidebarSection[];\n};\n\nconst FAMILY_ROW_CLASS =\n  \"flex w-full items-center justify-between px-3 py-1.5 rounded-md text-sm text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors\";\n\nfunction FamilyRowLabel({ section }: { section: SidebarSection }) {\n  return (\n    <>\n      <span>{section.title}</span>\n      <span className=\"flex items-center gap-1 text-xs\">\n        {section.items.length}\n        <ChevronRight className=\"size-3\" />\n      </span>\n    </>\n  );\n}\n\nfunction FamilyList({\n  isMobile,\n  onNavigate,\n  onOpen,\n  sections,\n}: {\n  isMobile: boolean;\n  onNavigate: () => void;\n  onOpen: (title: string) => void;\n  sections: SidebarSection[];\n}) {\n  return (\n    <div className=\"space-y-1\">\n      <div className=\"px-3 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wider\">\n        Components\n      </div>\n      <div className=\"space-y-0.5\">\n        {sections.map((section) =>\n          section.href ? (\n            <Link\n              className={FAMILY_ROW_CLASS}\n              href={section.href}\n              key={section.title}\n              onClick={() => {\n                if (isMobile) {\n                  onNavigate();\n                }\n              }}\n            >\n              <FamilyRowLabel section={section} />\n            </Link>\n          ) : (\n            <button\n              className={FAMILY_ROW_CLASS}\n              key={section.title}\n              onClick={() => {\n                onOpen(section.title ?? \"\");\n              }}\n              type=\"button\"\n            >\n              <FamilyRowLabel section={section} />\n            </button>\n          ),\n        )}\n      </div>\n    </div>\n  );\n}\n\nfunction FamilyItems({\n  isMobile,\n  onBack,\n  onNavigate,\n  pathname,\n  section,\n}: {\n  isMobile: boolean;\n  onBack: () => void;\n  onNavigate: () => void;\n  pathname: string;\n  section: SidebarSection;\n}) {\n  return (\n    <div className=\"space-y-1\">\n      <button\n        className=\"flex w-full items-center gap-1.5 px-3 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wider hover:text-foreground transition-colors\"\n        onClick={onBack}\n        type=\"button\"\n      >\n        <ChevronLeft className=\"size-3\" />\n        <span>All families</span>\n      </button>\n      <div className=\"flex items-center justify-between px-3 pb-1\">\n        {section.href ? (\n          <Link\n            className=\"text-sm font-semibold hover:underline\"\n            href={section.href}\n            onClick={() => {\n              if (isMobile) {\n                onNavigate();\n              }\n            }}\n          >\n            {section.title}\n          </Link>\n        ) : (\n          <span className=\"text-sm font-semibold\">{section.title}</span>\n        )}\n        <span className=\"text-xs text-muted-foreground\">\n          {section.items.length}\n        </span>\n      </div>\n      <div className=\"space-y-0.5\">\n        {section.items.map((item) => (\n          <Link\n            aria-current={pathname === item.href ? \"page\" : undefined}\n            className={cn(\n              \"block px-3 py-1.5 rounded-md text-sm transition-colors\",\n              pathname === item.href\n                ? \"bg-accent text-accent-foreground font-medium\"\n                : \"text-muted-foreground hover:bg-accent hover:text-accent-foreground\",\n            )}\n            href={item.href}\n            key={item.href}\n            onClick={() => {\n              if (isMobile) {\n                onNavigate();\n              }\n            }}\n          >\n            {item.title}\n          </Link>\n        ))}\n      </div>\n    </div>\n  );\n}\n\n/**\n * Single-pane drill-down for grouped component families: the family list (ROOT)\n * or one family's items (FAMILY), so a long flat list never renders at once.\n * Auto-drills into the family that contains the active route, re-syncing on\n * navigation while still allowing manual drill / back between routes.\n *\n * @param sections - family-tagged sidebar sections (each `title` is a family)\n * @param pathname - current route, used to detect + auto-open the active family\n */\nfunction FamilyNav({\n  isMobile,\n  onNavigate,\n  pathname,\n  sections,\n}: FamilyNavProps) {\n  const activeTitle =\n    sections.find(\n      (section) =>\n        section.href === pathname ||\n        section.items.some((item) => item.href === pathname),\n    )?.title ?? null;\n\n  const [routeKey, setRouteKey] = useState(pathname);\n  const [openTitle, setOpenTitle] = useState<null | string>(activeTitle);\n\n  if (routeKey !== pathname) {\n    setRouteKey(pathname);\n    setOpenTitle(activeTitle);\n  }\n\n  const openSection =\n    openTitle === null\n      ? null\n      : (sections.find((section) => section.title === openTitle) ?? null);\n\n  if (openSection) {\n    return (\n      <FamilyItems\n        isMobile={isMobile}\n        onBack={() => {\n          setOpenTitle(null);\n        }}\n        onNavigate={onNavigate}\n        pathname={pathname}\n        section={openSection}\n      />\n    );\n  }\n\n  return (\n    <FamilyList\n      isMobile={isMobile}\n      onNavigate={onNavigate}\n      onOpen={(title) => {\n        setOpenTitle(title);\n      }}\n      sections={sections}\n    />\n  );\n}\n\n// eslint-disable-next-line max-lines-per-function\nexport function Sidebar({ sections }: SidebarProps) {\n  const pathname = usePathname();\n  const { open, setOpen } = useSidebar();\n  const isMobile = useMobile(setOpen);\n  const mounted = useMounted();\n  const scrollContainerReference = useRef<HTMLDivElement>(null);\n  const { showBottomFade, showTopFade } = useScrollFade(\n    scrollContainerReference,\n  );\n\n  const collapsed = mounted && !isMobile && !open;\n\n  const familySections = sections.filter((section) => section.family);\n  const otherSections = sections.filter((section) => !section.family);\n\n  return (\n    <>\n      {/* Mobile overlay */}\n      {isMobile && open ? (\n        <div\n          className=\"fixed inset-0 bg-black/50 z-40 lg:hidden\"\n          data-testid=\"sidebar-overlay\"\n          onClick={() => {\n            setOpen(false);\n          }}\n          onKeyDown={(event) => {\n            if (event.key === \"Escape\") {\n              setOpen(false);\n            }\n          }}\n          role=\"button\"\n          tabIndex={0}\n        />\n      ) : null}\n\n      {/* Sidebar */}\n      <aside\n        className={cn(\n          \"fixed lg:relative top-16 lg:top-0 bottom-0 lg:bottom-auto left-0 z-40 lg:h-full bg-background transition-[transform,width] duration-300 ease-in-out\",\n          \"flex flex-col\",\n          \"overflow-hidden\",\n          \"shrink-0\",\n          collapsed ? \"border-r-0\" : \"border-r\",\n          collapsed ? \"w-0\" : isMobile ? \"w-full\" : \"w-64\",\n          isMobile && open && \"translate-x-0\",\n          isMobile && !open && \"-translate-x-full\",\n          !isMobile && !collapsed && \"-translate-x-full lg:translate-x-0\",\n          !isMobile && collapsed && \"-translate-x-full\",\n        )}\n      >\n        <div className=\"relative flex-1 overflow-hidden\">\n          {/* Top fade */}\n          {showTopFade ? (\n            <div className=\"absolute top-0 left-0 right-0 h-8 bg-gradient-to-b from-background to-transparent pointer-events-none z-20\" />\n          ) : null}\n\n          {/* Bottom fade */}\n          {showBottomFade ? (\n            <div className=\"absolute bottom-0 left-0 right-0 h-8 bg-gradient-to-t from-background to-transparent pointer-events-none z-20\" />\n          ) : null}\n\n          <nav\n            className=\"flex-1 p-4 overflow-y-auto overscroll-contain h-full [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\"\n            ref={scrollContainerReference}\n          >\n            <div className=\"space-y-4\">\n              {otherSections.map((section, sectionIndex) => {\n                const sectionItems = (\n                  <div className={section.title ? \"space-y-0.5\" : \"space-y-1\"}>\n                    {section.items.map((item) => (\n                      <Link\n                        className={cn(\n                          section.title\n                            ? \"block px-3 py-1.5 rounded-md text-sm transition-colors\"\n                            : \"flex items-center px-3 py-2 rounded-md text-sm font-medium transition-colors\",\n                          pathname === item.href ||\n                            (item.href === \"/\" && pathname === \"/\")\n                            ? \"bg-accent text-accent-foreground\"\n                            : section.title\n                              ? \"text-muted-foreground hover:bg-accent hover:text-accent-foreground\"\n                              : \"hover:bg-accent hover:text-accent-foreground\",\n                          section.title &&\n                            pathname === item.href &&\n                            \"font-medium\",\n                        )}\n                        href={item.href}\n                        key={item.href}\n                        onClick={() => {\n                          if (isMobile) {\n                            setOpen(false);\n                          }\n                        }}\n                      >\n                        {item.title}\n                      </Link>\n                    ))}\n                  </div>\n                );\n\n                return (\n                  <div\n                    className=\"space-y-1\"\n                    key={section.title || sectionIndex}\n                  >\n                    {section.title ? (\n                      <CollapsibleSection\n                        collapsible={section.collapsible}\n                        defaultOpen={section.defaultOpen ?? true}\n                        title={section.title}\n                      >\n                        {sectionItems}\n                      </CollapsibleSection>\n                    ) : (\n                      sectionItems\n                    )}\n                  </div>\n                );\n              })}\n              {familySections.length > 0 ? (\n                <FamilyNav\n                  isMobile={isMobile}\n                  onNavigate={() => {\n                    setOpen(false);\n                  }}\n                  pathname={pathname}\n                  sections={familySections}\n                />\n              ) : null}\n            </div>\n          </nav>\n        </div>\n      </aside>\n    </>\n  );\n}\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component",
  "version": "0.3.0",
  "stability": "stable"
}
