{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "docs-sidebar",
  "title": "Docs sidebar",
  "description": "SidebarPageTree renders a Fumadocs page tree (pages, folders, separators, icons, external links) with the shadcn/ui sidebar menu, and SidebarLinkItem renders layout links.",
  "dependencies": [
    "cn",
    "@fuma-translate/react@^1.0.2",
    "fumadocs-core@^16.17.8",
    "lucide-react",
    "scroll-into-view-if-needed"
  ],
  "registryDependencies": [
    "sidebar",
    "collapsible",
    "@docscn/tree-context",
    "@docscn/layout-shared",
    "@docscn/docs-utils"
  ],
  "files": [
    {
      "path": "registry/base/docs/components/sidebar/page-tree.tsx",
      "content": "// Adapted from Fumadocs UI (https://github.com/fuma-nama/fumadocs)\n// Copyright (c) 2023 Fuma, MIT License\n\"use client\";\n\nimport { useTranslations } from \"@fuma-translate/react\";\nimport { cn } from \"cn\";\nimport { usePathname } from \"fumadocs-core/framework\";\nimport Link from \"fumadocs-core/link\";\nimport type * as PageTree from \"fumadocs-core/page-tree\";\nimport { ChevronRight } from \"lucide-react\";\nimport {\n  type ComponentProps,\n  createContext,\n  type FC,\n  Fragment,\n  type ReactNode,\n  type RefCallback,\n  use,\n  useCallback,\n  useMemo,\n  useState,\n} from \"react\";\nimport scrollIntoView from \"scroll-into-view-if-needed\";\nimport {\n  Collapsible,\n  CollapsibleContent,\n  CollapsibleTrigger,\n} from \"@/components/ui/collapsible\";\nimport {\n  SidebarGroup,\n  SidebarGroupContent,\n  SidebarGroupLabel,\n  SidebarMenu,\n  SidebarMenuAction,\n  SidebarMenuButton,\n  SidebarMenuItem,\n  SidebarMenuSub,\n  SidebarMenuSubButton,\n  SidebarMenuSubItem,\n  useSidebar,\n} from \"@/components/ui/sidebar\";\nimport { useTreeContext, useTreePath } from \"../../contexts/tree\";\nimport { isActive } from \"../../utils/urls\";\n\nexport interface SidebarPageTreeComponents {\n  Item: FC<{ item: PageTree.Item }>;\n  Folder: FC<{ item: PageTree.Folder; children: ReactNode }>;\n  Separator: FC<{ item: PageTree.Separator }>;\n}\n\nexport interface SidebarTreeOptions {\n  /**\n   * Open folders up to this depth by default\n   *\n   * @defaultValue 0\n   */\n  defaultOpenLevel?: number;\n\n  /**\n   * Prefetch the pages linked from the sidebar\n   */\n  prefetch?: boolean;\n}\n\nconst OptionsContext = createContext<SidebarTreeOptions>({});\nconst DepthContext = createContext(0);\n\n/**\n * Options for the sidebar items below it (`defaultOpenLevel` and `prefetch`).\n */\nexport function SidebarTreeOptionsProvider({\n  defaultOpenLevel = 0,\n  prefetch,\n  children,\n}: SidebarTreeOptions & { children: ReactNode }) {\n  return (\n    <OptionsContext\n      value={useMemo(\n        () => ({ defaultOpenLevel, prefetch }),\n        [defaultOpenLevel, prefetch],\n      )}\n    >\n      {children}\n    </OptionsContext>\n  );\n}\n\n/** Scroll the active item into view inside the sidebar when it mounts */\nfunction useScrollIntoView(active: boolean): RefCallback<HTMLElement> {\n  return useCallback(\n    (element: HTMLElement | null) => {\n      if (!active || !element) return;\n      const boundary = element.closest<HTMLElement>(\n        '[data-slot=\"sidebar-content\"]',\n      );\n      if (!boundary) return;\n\n      scrollIntoView(element, {\n        boundary,\n        block: \"center\",\n        scrollMode: \"if-needed\",\n      });\n    },\n    [active],\n  );\n}\n\n/** Close the mobile sidebar after navigating */\nfunction useCloseOnNavigate() {\n  const { isMobile, setOpenMobile } = useSidebar();\n  return () => {\n    if (isMobile) setOpenMobile(false);\n  };\n}\n\nexport function SidebarItem({\n  href,\n  external,\n  active = false,\n  icon,\n  children,\n  className,\n  onClick,\n  ...props\n}: Omit<ComponentProps<\"a\">, \"href\"> & {\n  href: string;\n  external?: boolean;\n  active?: boolean;\n  icon?: ReactNode;\n}) {\n  const depth = use(DepthContext);\n  const { prefetch } = use(OptionsContext);\n  const closeOnNavigate = useCloseOnNavigate();\n  const ref = useScrollIntoView(active);\n  const link = (\n    <Link\n      ref={ref}\n      href={href}\n      external={external}\n      prefetch={prefetch}\n      onClick={(e) => {\n        onClick?.(e);\n        closeOnNavigate();\n      }}\n      {...props}\n    />\n  );\n\n  if (depth === 0) {\n    return (\n      <SidebarMenuItem>\n        <SidebarMenuButton\n          render={link}\n          isActive={active}\n          className={className}\n        >\n          {icon}\n          <span>{children}</span>\n        </SidebarMenuButton>\n      </SidebarMenuItem>\n    );\n  }\n\n  return (\n    <SidebarMenuSubItem>\n      <SidebarMenuSubButton\n        render={link}\n        isActive={active}\n        className={className}\n      >\n        {icon}\n        <span>{children}</span>\n      </SidebarMenuSubButton>\n    </SidebarMenuSubItem>\n  );\n}\n\nexport function SidebarFolder({\n  name,\n  icon,\n  index,\n  active = false,\n  defaultOpen,\n  collapsible = true,\n  children,\n}: {\n  name: ReactNode;\n  icon?: ReactNode;\n  /** the folder's own page */\n  index?: { url: string; external?: boolean; active: boolean };\n  /** whether the current page is inside the folder */\n  active?: boolean;\n  defaultOpen?: boolean;\n  collapsible?: boolean;\n  children: ReactNode;\n}) {\n  const depth = use(DepthContext);\n  const { defaultOpenLevel = 0, prefetch } = use(OptionsContext);\n  const closeOnNavigate = useCloseOnNavigate();\n  const ref = useScrollIntoView(index?.active ?? false);\n  const t = useTranslations({ note: \"sidebar\" });\n  const [open, setOpen] = useState(\n    () => active || (defaultOpen ?? depth < defaultOpenLevel),\n  );\n  // open the folder when navigating into it\n  const [prevActive, setPrevActive] = useState(active);\n  if (prevActive !== active) {\n    setPrevActive(active);\n    if (active) setOpen(true);\n  }\n\n  const isOpen = !collapsible || open;\n  const Item = depth === 0 ? SidebarMenuItem : SidebarMenuSubItem;\n  const Button = depth === 0 ? SidebarMenuButton : SidebarMenuSubButton;\n  const chevron = (\n    <ChevronRight\n      className={cn(\n        \"size-4 shrink-0 transition-transform\",\n        isOpen && \"rotate-90\",\n      )}\n    />\n  );\n\n  let trigger: ReactNode;\n  if (index) {\n    trigger = (\n      <>\n        <Button\n          render={\n            <Link\n              ref={ref}\n              href={index.url}\n              external={index.external}\n              prefetch={prefetch}\n              onClick={() => {\n                setOpen(true);\n                closeOnNavigate();\n              }}\n            />\n          }\n          isActive={index.active}\n          className={cn(collapsible && \"pe-8\")}\n        >\n          {icon}\n          <span>{name}</span>\n        </Button>\n        {collapsible && (\n          <CollapsibleTrigger\n            render={\n              <SidebarMenuAction\n                className={cn(depth > 0 && \"top-0.5 right-0.5\")}\n              />\n            }\n            aria-label={\n              open\n                ? t(\"Collapse\", { note: \"aria-label\" })\n                : t(\"Expand\", { note: \"aria-label\" })\n            }\n          >\n            {chevron}\n          </CollapsibleTrigger>\n        )}\n      </>\n    );\n  } else if (collapsible) {\n    trigger = (\n      <CollapsibleTrigger\n        render={<Button render={<button type=\"button\" />} className=\"w-full\" />}\n      >\n        {icon}\n        <span className=\"truncate\">{name}</span>\n        <span className=\"ms-auto flex\">{chevron}</span>\n      </CollapsibleTrigger>\n    );\n  } else {\n    trigger = (\n      <Button render={<div />}>\n        {icon}\n        <span>{name}</span>\n      </Button>\n    );\n  }\n\n  return (\n    <Collapsible open={isOpen} onOpenChange={setOpen} render={<Item />}>\n      {trigger}\n      <CollapsibleContent className=\"h-(--collapsible-panel-height) overflow-hidden transition-[height] duration-200 ease-out data-ending-style:h-0 data-starting-style:h-0\">\n        <SidebarMenuSub className=\"me-0 pe-0\">\n          <DepthContext value={depth + 1}>{children}</DepthContext>\n        </SidebarMenuSub>\n      </CollapsibleContent>\n    </Collapsible>\n  );\n}\n\nexport function SidebarSeparator({\n  className,\n  children,\n  ...props\n}: ComponentProps<\"li\">) {\n  return (\n    <li\n      role=\"presentation\"\n      className={cn(\n        \"flex items-center gap-2 px-2 pt-4 pb-1 text-xs font-medium text-sidebar-foreground/70 first:pt-0 [&_svg]:size-4 [&_svg]:shrink-0\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </li>\n  );\n}\n\nconst RendererContext = createContext<\n  | (Partial<SidebarPageTreeComponents> & {\n      pathname: string;\n    })\n  | null\n>(null);\n\nfunction renderList(nodes: PageTree.Node[]) {\n  return nodes.map((node, i) => (\n    <PageTreeNode key={node.$id ?? i} node={node} />\n  ));\n}\n\nfunction PageTreeNode({ node }: { node: PageTree.Node }) {\n  const { Separator, Item, Folder, pathname } = use(RendererContext)!;\n  const path = useTreePath();\n\n  if (node.type === \"separator\") {\n    if (Separator) return <Separator item={node} />;\n    return (\n      <SidebarSeparator>\n        {node.icon}\n        {node.name}\n      </SidebarSeparator>\n    );\n  }\n\n  if (node.type === \"folder\") {\n    if (Folder) return <Folder item={node}>{renderList(node.children)}</Folder>;\n\n    return (\n      <SidebarFolder\n        name={node.name}\n        icon={node.icon}\n        index={\n          node.index && {\n            url: node.index.url,\n            external: node.index.external,\n            active: isActive(node.index.url, pathname),\n          }\n        }\n        active={path.includes(node)}\n        defaultOpen={node.defaultOpen}\n        collapsible={node.collapsible}\n      >\n        {renderList(node.children)}\n      </SidebarFolder>\n    );\n  }\n\n  if (Item) return <Item item={node} />;\n  return (\n    <SidebarItem\n      href={node.url}\n      external={node.external}\n      active={isActive(node.url, pathname)}\n      icon={node.icon}\n    >\n      {node.name}\n    </SidebarItem>\n  );\n}\n\n/**\n * Group the top-level nodes by separator, so each separator becomes a sidebar group label.\n */\nfunction groupBySeparator(nodes: PageTree.Node[]) {\n  const groups: { separator?: PageTree.Separator; nodes: PageTree.Node[] }[] = [\n    { nodes: [] },\n  ];\n\n  for (const node of nodes) {\n    if (node.type === \"separator\") groups.push({ separator: node, nodes: [] });\n    else groups[groups.length - 1]!.nodes.push(node);\n  }\n\n  return groups.filter((group) => group.separator || group.nodes.length > 0);\n}\n\n/**\n * Render sidebar items from page tree\n */\nexport function SidebarPageTree(\n  components: Partial<SidebarPageTreeComponents>,\n) {\n  const { Folder, Item, Separator } = components;\n  const { root } = useTreeContext();\n  const pathname = usePathname();\n\n  return (\n    <RendererContext\n      value={useMemo(\n        () => ({ Folder, Item, Separator, pathname }),\n        [Folder, Item, Separator, pathname],\n      )}\n    >\n      <Fragment key={root.$id}>\n        {groupBySeparator(root.children).map((group, i) => (\n          <SidebarGroup key={group.separator?.$id ?? i}>\n            {group.separator &&\n              (Separator ? (\n                <Separator item={group.separator} />\n              ) : (\n                <SidebarGroupLabel className=\"gap-2\">\n                  {group.separator.icon}\n                  {group.separator.name}\n                </SidebarGroupLabel>\n              ))}\n            <SidebarGroupContent>\n              <SidebarMenu>{renderList(group.nodes)}</SidebarMenu>\n            </SidebarGroupContent>\n          </SidebarGroup>\n        ))}\n      </Fragment>\n    </RendererContext>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/docs/components/sidebar/page-tree.tsx"
    },
    {
      "path": "registry/base/docs/components/sidebar/link-item.tsx",
      "content": "// Adapted from Fumadocs UI (https://github.com/fuma-nama/fumadocs)\n// Copyright (c) 2023 Fuma, MIT License\n\"use client\";\n\nimport { usePathname } from \"fumadocs-core/framework\";\nimport type { HTMLAttributes } from \"react\";\nimport { isLinkItemActive, type LinkItemType } from \"../../layouts/shared\";\nimport { SidebarFolder, SidebarItem } from \"./page-tree\";\n\n/**\n * Render a link item (from the layout's `links` option) in the sidebar\n */\nexport function SidebarLinkItem({\n  item,\n  ...props\n}: HTMLAttributes<HTMLElement> & {\n  item: Exclude<LinkItemType, { type: \"icon\" }>;\n}) {\n  const pathname = usePathname();\n  const active = isLinkItemActive(item, pathname);\n\n  if (item.type === \"custom\")\n    return (\n      <li className={props.className} style={props.style}>\n        {item.children}\n      </li>\n    );\n\n  if (item.type === \"menu\")\n    return (\n      <SidebarFolder\n        name={item.text}\n        icon={item.icon}\n        index={\n          item.url\n            ? { url: item.url, external: item.external, active }\n            : undefined\n        }\n      >\n        {item.items.map((child, i) => (\n          <SidebarLinkItem key={i} item={child} />\n        ))}\n      </SidebarFolder>\n    );\n\n  return (\n    <SidebarItem\n      href={item.url}\n      icon={item.icon}\n      external={item.external}\n      active={active}\n      className={props.className}\n    >\n      {item.text}\n    </SidebarItem>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/docs/components/sidebar/link-item.tsx"
    },
    {
      "path": "registry/base/docs/components/sidebar/base.tsx",
      "content": "// Adapted from Fumadocs UI (https://github.com/fuma-nama/fumadocs)\n// Copyright (c) 2023 Fuma, MIT License\n\"use client\";\n\nimport { useTranslations } from \"@fuma-translate/react\";\nimport type { ComponentProps } from \"react\";\nimport {\n  SidebarRail as SidebarRailPrimitive,\n  SidebarTrigger as SidebarTriggerPrimitive,\n  useSidebar,\n} from \"@/components/ui/sidebar\";\n\n/**\n * The label for a button that toggles the sidebar, using Fumadocs UI's\n * translation keys (shadcn/ui's sidebar labels it \"Toggle Sidebar\" in English).\n */\nfunction useToggleLabel() {\n  const { isMobile, open, openMobile } = useSidebar();\n  const t = useTranslations({ note: \"sidebar\" });\n\n  if (isMobile ? !openMobile : !open)\n    return t(\"Open Sidebar\", { note: \"aria-label\" });\n  return isMobile\n    ? t(\"Close Sidebar\", { note: \"aria-label\" })\n    : t(\"Collapse Sidebar\", { note: \"aria-label\" });\n}\n\n/**\n * shadcn/ui's `SidebarTrigger`, with a translated label.\n */\nexport function SidebarTrigger(\n  props: ComponentProps<typeof SidebarTriggerPrimitive>,\n) {\n  return <SidebarTriggerPrimitive aria-label={useToggleLabel()} {...props} />;\n}\n\n/**\n * shadcn/ui's `SidebarRail`, with a translated label.\n */\nexport function SidebarRail(\n  props: ComponentProps<typeof SidebarRailPrimitive>,\n) {\n  const label = useToggleLabel();\n\n  return <SidebarRailPrimitive aria-label={label} title={label} {...props} />;\n}\n",
      "type": "registry:component",
      "target": "@components/docs/components/sidebar/base.tsx"
    }
  ],
  "type": "registry:component"
}