{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "docs-layout",
  "title": "Docs layout",
  "description": "DocsLayout: the docs layout on the shadcn/ui sidebar, with the page tree, search, layout tabs, links, a theme switch and a mobile navbar.",
  "dependencies": [
    "cn",
    "fumadocs-core@^16.17.8",
    "lucide-react"
  ],
  "registryDependencies": [
    "sidebar",
    "button",
    "@docscn/docs-sidebar",
    "@docscn/sidebar-tabs",
    "@docscn/theme-switch",
    "@docscn/search-trigger",
    "@docscn/tree-context",
    "@docscn/layout-shared",
    "@docscn/docs-utils",
    "@docscn/i18n-context",
    "@docscn/language-select"
  ],
  "files": [
    {
      "path": "registry/base/docs/layouts/docs/index.tsx",
      "content": "// Adapted from Fumadocs UI (https://github.com/fuma-nama/fumadocs)\n// Copyright (c) 2023 Fuma, MIT License\nimport type * as PageTree from \"fumadocs-core/page-tree\";\nimport type { ComponentProps } from \"react\";\nimport type { SidebarTreeOptions } from \"../../components/sidebar/page-tree\";\nimport {\n  type AIChatOptions,\n  type BaseLayoutProps,\n  getLayoutTabs,\n  type GetLayoutTabsOptions,\n  type LayoutTab,\n} from \"../shared\";\nimport { LayoutBody } from \"./client\";\nimport type { SidebarProps } from \"./slots/sidebar\";\n\nexport interface DocsLayoutProps extends BaseLayoutProps {\n  tree: PageTree.Root;\n  sidebar?: SidebarOptions;\n  tabMode?: \"top\" | \"auto\";\n  tabs?: LayoutTab[] | GetLayoutTabsOptions | false;\n  containerProps?: ComponentProps<\"main\">;\n  aiChat?: AIChatOptions;\n}\n\nexport interface SidebarOptions extends SidebarProps, SidebarTreeOptions {\n  /**\n   * Show the sidebar\n   *\n   * @defaultValue true\n   */\n  enabled?: boolean;\n\n  /**\n   * Whether the sidebar is open on desktop at first. Read the `sidebar_state`\n   * cookie in your layout and pass it here to keep the state across page loads.\n   *\n   * @defaultValue true\n   */\n  defaultOpen?: boolean;\n\n  /** Control the desktop open state */\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n}\n\nexport function DocsLayout({\n  tree,\n  tabs: layoutTabs,\n  children,\n  ...props\n}: DocsLayoutProps) {\n  let tabs: LayoutTab[] = [];\n  if (Array.isArray(layoutTabs)) {\n    tabs = layoutTabs;\n  } else if (typeof layoutTabs === \"object\") {\n    tabs = getLayoutTabs(tree, layoutTabs);\n  } else if (layoutTabs !== false) {\n    tabs = getLayoutTabs(tree);\n  }\n\n  return (\n    <LayoutBody tree={tree} tabs={tabs} {...props}>\n      {children}\n    </LayoutBody>\n  );\n}\n\nexport { useDocsLayout } from \"./client\";\n",
      "type": "registry:component",
      "target": "@components/docs/layouts/docs/index.tsx"
    },
    {
      "path": "registry/base/docs/layouts/docs/client.tsx",
      "content": "// Adapted from Fumadocs UI (https://github.com/fuma-nama/fumadocs)\n// Copyright (c) 2023 Fuma, MIT License\n\"use client\";\n\nimport { cn } from \"cn\";\nimport { usePathname } from \"fumadocs-core/framework\";\nimport Link from \"fumadocs-core/link\";\nimport { type ComponentProps, createContext, use, useMemo } from \"react\";\nimport { SidebarProvider } from \"@/components/ui/sidebar\";\nimport { SidebarTreeOptionsProvider } from \"../../components/sidebar/page-tree\";\nimport {\n  TreeContextProvider,\n  useTabsGroups,\n  useTreePath,\n} from \"../../contexts/tree\";\nimport { useIsScrollTop } from \"../../utils/use-is-scroll-top\";\nimport {\n  isLayoutTabActive,\n  type LayoutTab,\n  type LinkItemType,\n  useLinkItems,\n} from \"../shared\";\nimport { AIChatPanel, AIChatTrigger, SkipToContent } from \"../shared/client\";\nimport type { DocsLayoutProps } from \".\";\nimport { Container } from \"./slots/container\";\nimport { CollapsedSidebarPanel, Header } from \"./slots/header\";\nimport { Sidebar } from \"./slots/sidebar\";\nimport { findLast } from \"../../utils/array\";\n\ninterface LayoutProps extends Pick<\n  DocsLayoutProps,\n  \"nav\" | \"themeSwitch\" | \"searchToggle\" | \"aiChat\"\n> {\n  tabs: LayoutTab[];\n  tabMode: NonNullable<DocsLayoutProps[\"tabMode\"]>;\n}\n\nconst LayoutContext = createContext<{\n  props: LayoutProps;\n  isNavTransparent: boolean;\n  sidebarEnabled: boolean;\n  navItems: LinkItemType[];\n  menuItems: LinkItemType[];\n} | null>(null);\n\nexport function useIsDocsLayout() {\n  return use(LayoutContext) !== null;\n}\n\nexport function useDocsLayout() {\n  const context = use(LayoutContext);\n  if (!context)\n    throw new Error(\n      \"Please use <DocsPage /> (`components/docs/layouts/docs/page`) under <DocsLayout /> (`components/docs/layouts/docs`).\",\n    );\n  return context;\n}\n\nexport function LayoutBody(\n  props: Omit<DocsLayoutProps, \"tabs\"> & {\n    tabs: LayoutTab[];\n  },\n) {\n  const {\n    nav,\n    themeSwitch,\n    searchToggle,\n    sidebar: {\n      enabled: sidebarEnabled = true,\n      defaultOpenLevel,\n      prefetch,\n      defaultOpen,\n      open,\n      onOpenChange,\n      collapsible = true,\n      ...sidebarProps\n    } = {},\n    tabs,\n    tabMode = \"auto\",\n    tree,\n    containerProps,\n    aiChat,\n    children,\n  } = props;\n  const { enabled: navEnabled = true, transparentMode = \"none\" } = nav ?? {};\n  const isTop = useIsScrollTop({ enabled: transparentMode === \"top\" }) ?? true;\n  const isNavTransparent =\n    transparentMode === \"top\" ? isTop : transparentMode === \"always\";\n  const linkItems = useLinkItems(props);\n\n  return (\n    <TreeContextProvider tree={tree}>\n      <LayoutContext\n        value={{\n          props: { tabMode, tabs, nav, themeSwitch, searchToggle, aiChat },\n          isNavTransparent,\n          sidebarEnabled,\n          ...linkItems,\n        }}\n      >\n        <SidebarTreeOptionsProvider\n          defaultOpenLevel={defaultOpenLevel}\n          prefetch={prefetch}\n        >\n          <SidebarProvider\n            defaultOpen={defaultOpen}\n            open={collapsible ? open : true}\n            onOpenChange={onOpenChange}\n          >\n            <SkipToContent />\n            {sidebarEnabled && (\n              <Sidebar collapsible={collapsible} {...sidebarProps} />\n            )}\n            <Container\n              navEnabled={navEnabled}\n              sidebarCollapsible={sidebarEnabled && collapsible}\n              {...containerProps}\n            >\n              {navEnabled && <Header />}\n              {sidebarEnabled && collapsible && <CollapsedSidebarPanel />}\n              {tabMode === \"top\" && tabs.length > 0 && (\n                <LayoutTabs\n                  tabs={tabs}\n                  className=\"z-10 border-b bg-background px-6 pt-3 max-md:hidden xl:px-8\"\n                />\n              )}\n              {children}\n            </Container>\n            {aiChat?.panel && (\n              <AIChatPanel\n                open={aiChat.open}\n                className=\"xl:sticky xl:top-(--docs-banner-height,0px) xl:h-[calc(100svh-var(--docs-banner-height,0px))] xl:data-[state=open]:border-s\"\n              >\n                {aiChat.panel}\n              </AIChatPanel>\n            )}\n            {aiChat?.trigger && <AIChatTrigger {...aiChat} />}\n          </SidebarProvider>\n        </SidebarTreeOptionsProvider>\n      </LayoutContext>\n    </TreeContextProvider>\n  );\n}\n\nfunction LayoutTabs({\n  tabs: allTabs,\n  ...props\n}: ComponentProps<\"div\"> & {\n  tabs: LayoutTab[];\n}) {\n  const pathname = usePathname();\n  const path = useTreePath();\n  const group = findLast(\n    useTabsGroups(allTabs),\n    (group) => typeof group.active?.root !== \"string\",\n  );\n  const selected = useMemo(() => {\n    if (!group) return;\n    return findLast(group.options, (option) =>\n      isLayoutTabActive(option, path, pathname),\n    );\n  }, [group, path, pathname]);\n  if (!group) return;\n\n  return (\n    <div\n      {...props}\n      className={cn(\n        \"no-scrollbar flex flex-row items-end gap-6 overflow-auto\",\n        props.className,\n      )}\n    >\n      {group.options.map((tab, i) => (\n        <Link\n          key={i}\n          href={tab.url}\n          className={cn(\n            \"inline-flex items-center gap-2 border-b-2 border-transparent pb-1.5 text-sm font-medium text-nowrap text-muted-foreground transition-colors hover:text-foreground\",\n            tab.unlisted && selected !== tab && \"hidden\",\n            selected === tab && \"border-primary text-foreground\",\n          )}\n        >\n          {tab.title}\n        </Link>\n      ))}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/docs/layouts/docs/client.tsx"
    },
    {
      "path": "registry/base/docs/layouts/docs/slots/header.tsx",
      "content": "// Adapted from Fumadocs UI (https://github.com/fuma-nama/fumadocs)\n// Copyright (c) 2023 Fuma, MIT License\n\"use client\";\n\nimport { cn } from \"cn\";\nimport type { ComponentProps } from \"react\";\nimport { useSidebar } from \"@/components/ui/sidebar\";\nimport { SidebarTrigger } from \"../../../components/sidebar/base\";\nimport { NavTitle } from \"../../shared/client\";\nimport { SearchTrigger } from \"../../shared/slots/search-trigger\";\nimport { useDocsLayout } from \"../client\";\n\n/**\n * The navbar on mobile, with the title, search and the sidebar trigger.\n */\nexport function Header(props: ComponentProps<\"header\">) {\n  const {\n    isNavTransparent,\n    sidebarEnabled,\n    props: { nav, searchToggle },\n  } = useDocsLayout();\n  const searchEnabled = searchToggle?.enabled ?? true;\n\n  return (\n    <header\n      id=\"nd-subnav\"\n      data-transparent={isNavTransparent}\n      {...props}\n      className={cn(\n        \"sticky top-(--docs-banner-height,0px) z-30 flex h-(--docs-header-height) items-center gap-2 border-b ps-4 pe-2.5 backdrop-blur-sm transition-colors data-[transparent=false]:bg-background/80 md:hidden\",\n        props.className,\n      )}\n    >\n      <NavTitle\n        nav={nav}\n        className=\"inline-flex items-center gap-2.5 font-semibold\"\n      />\n      <div className=\"flex-1\">{nav?.children}</div>\n      {searchEnabled && <SearchTrigger hideIfDisabled {...searchToggle?.sm} />}\n      {sidebarEnabled && <SidebarTrigger />}\n    </header>\n  );\n}\n\n/**\n * Shown on desktop while the sidebar is collapsed, to open it again.\n */\nexport function CollapsedSidebarPanel({\n  className,\n  ...props\n}: ComponentProps<\"div\">) {\n  const { state, isMobile } = useSidebar();\n  const {\n    props: { searchToggle },\n  } = useDocsLayout();\n  const collapsed = state === \"collapsed\" && !isMobile;\n\n  return (\n    <div\n      data-sidebar-panel=\"\"\n      inert={!collapsed}\n      {...props}\n      className={cn(\n        \"fixed start-4 top-[calc(var(--docs-banner-height,0px)+--spacing(4))] z-20 flex rounded-xl border bg-muted p-0.5 text-muted-foreground shadow-lg transition-opacity max-md:hidden\",\n        !collapsed && \"pointer-events-none opacity-0\",\n        className,\n      )}\n    >\n      <SidebarTrigger className=\"rounded-lg\" />\n      {(searchToggle?.enabled ?? true) && (\n        <SearchTrigger hideIfDisabled className=\"rounded-lg\" />\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/docs/layouts/docs/slots/header.tsx"
    },
    {
      "path": "registry/base/docs/layouts/docs/slots/sidebar.tsx",
      "content": "// Adapted from Fumadocs UI (https://github.com/fuma-nama/fumadocs)\n// Copyright (c) 2023 Fuma, MIT License\n\"use client\";\n\nimport { cn } from \"cn\";\nimport { ChevronDown, Languages } from \"lucide-react\";\nimport type { ComponentProps, ReactNode } from \"react\";\nimport { buttonVariants } from \"@/components/ui/button\";\nimport {\n  Sidebar as SidebarRoot,\n  SidebarContent,\n  SidebarFooter,\n  SidebarGroup,\n  SidebarGroupContent,\n  SidebarHeader,\n  SidebarMenu,\n} from \"@/components/ui/sidebar\";\nimport { SidebarRail, SidebarTrigger } from \"../../../components/sidebar/base\";\nimport { SidebarLinkItem } from \"../../../components/sidebar/link-item\";\nimport {\n  SidebarPageTree,\n  type SidebarPageTreeComponents,\n} from \"../../../components/sidebar/page-tree\";\nimport { SidebarTabsDropdown } from \"../../../components/sidebar/tabs/dropdown\";\nimport { useI18n } from \"../../../contexts/i18n\";\nimport { LinkItem, NavTitle } from \"../../shared/client\";\nimport {\n  LanguageSelect,\n  LanguageSelectText,\n} from \"../../shared/slots/language-select\";\nimport { FullSearchTrigger } from \"../../shared/slots/search-trigger\";\nimport { ThemeSwitch } from \"../../shared/slots/theme-switch\";\nimport { useDocsLayout } from \"../client\";\n\nexport interface SidebarProps extends Omit<\n  ComponentProps<typeof SidebarRoot>,\n  \"collapsible\"\n> {\n  components?: Partial<SidebarPageTreeComponents>;\n  banner?: ReactNode;\n  footer?: ReactNode;\n\n  /**\n   * Support collapsing the sidebar on desktop mode\n   *\n   * @defaultValue true\n   */\n  collapsible?: boolean;\n}\n\nexport function Sidebar({\n  footer,\n  banner,\n  collapsible = true,\n  components,\n  className,\n  ...rest\n}: SidebarProps) {\n  const {\n    menuItems,\n    props: { tabs, nav, tabMode, themeSwitch, searchToggle },\n  } = useDocsLayout();\n  const iconLinks = menuItems.filter((item) => item.type === \"icon\");\n  const links = menuItems.filter((item) => item.type !== \"icon\");\n  const { enabled: themeSwitchEnabled = true, ...themeSwitchProps } =\n    themeSwitch ?? {};\n  const { enabled: searchEnabled = true, full: searchProps } =\n    searchToggle ?? {};\n  const { locales = [] } = useI18n();\n  const languageSelect = locales.length > 1;\n\n  return (\n    <SidebarRoot\n      collapsible=\"offcanvas\"\n      className={cn(\n        // below a Banner, if there is one\n        \"top-(--docs-banner-height,0px) h-[calc(100svh-var(--docs-banner-height,0px))] text-sm\",\n        className,\n      )}\n      {...rest}\n    >\n      <SidebarHeader className=\"gap-3 p-4 pb-2\">\n        <div className=\"flex items-center gap-2\">\n          <NavTitle\n            nav={nav}\n            className=\"ms-2 me-auto inline-flex items-center gap-2.5 text-[0.9375rem] font-medium\"\n          />\n          {nav?.children}\n          {collapsible && (\n            <SidebarTrigger className=\"-me-1.5 text-muted-foreground max-md:hidden\" />\n          )}\n        </div>\n        {searchEnabled && <FullSearchTrigger hideIfDisabled {...searchProps} />}\n        {tabs.length > 0 && (\n          <SidebarTabsDropdown\n            options={\n              tabMode === \"auto\"\n                ? tabs\n                : tabs.filter((tab) => typeof tab.$folder?.root === \"string\")\n            }\n          />\n        )}\n        {banner}\n      </SidebarHeader>\n      <SidebarContent className=\"gap-0 px-2\">\n        {links.length > 0 && (\n          <SidebarGroup>\n            <SidebarGroupContent>\n              <SidebarMenu>\n                {links.map((item, i) => (\n                  <SidebarLinkItem key={i} item={item} />\n                ))}\n              </SidebarMenu>\n            </SidebarGroupContent>\n          </SidebarGroup>\n        )}\n        <SidebarPageTree {...components} />\n      </SidebarContent>\n      {(languageSelect ||\n        iconLinks.length > 0 ||\n        themeSwitchEnabled ||\n        footer) && (\n        <SidebarFooter className=\"p-4 pt-2\">\n          {languageSelect && (\n            <LanguageSelect\n              variant=\"outline\"\n              className=\"justify-start text-muted-foreground\"\n            >\n              <Languages />\n              <LanguageSelectText />\n              <ChevronDown className=\"ms-auto size-3.5\" />\n            </LanguageSelect>\n          )}\n          {(iconLinks.length > 0 || themeSwitchEnabled) && (\n            <div className=\"flex items-center text-muted-foreground\">\n              {iconLinks.map((item, i) => (\n                <LinkItem\n                  key={i}\n                  item={item}\n                  className={cn(\n                    buttonVariants({ size: \"icon-sm\", variant: \"ghost\" }),\n                    \"[&_svg]:size-4\",\n                  )}\n                  aria-label={item.label}\n                >\n                  {item.icon}\n                </LinkItem>\n              ))}\n              {themeSwitchEnabled && (\n                <ThemeSwitch className=\"ms-auto\" {...themeSwitchProps} />\n              )}\n            </div>\n          )}\n          {footer}\n        </SidebarFooter>\n      )}\n      {collapsible && <SidebarRail />}\n    </SidebarRoot>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/docs/layouts/docs/slots/sidebar.tsx"
    },
    {
      "path": "registry/base/docs/layouts/docs/slots/container.tsx",
      "content": "// Adapted from Fumadocs UI (https://github.com/fuma-nama/fumadocs)\n// Copyright (c) 2023 Fuma, MIT License\n\"use client\";\n\nimport { cn } from \"cn\";\nimport type { ComponentProps } from \"react\";\nimport { SidebarInset } from \"@/components/ui/sidebar\";\n\n/**\n * The area beside the sidebar. It sets `--docs-header-height`, the height of the\n * mobile navbar, which pages use to offset sticky elements, and\n * `--docs-page-offset`, the space taken by the collapsed sidebar's panel.\n */\nexport function Container({\n  navEnabled = true,\n  sidebarCollapsible = true,\n  className,\n  ...props\n}: ComponentProps<\"main\"> & {\n  navEnabled?: boolean;\n  sidebarCollapsible?: boolean;\n}) {\n  return (\n    <SidebarInset\n      id=\"nd-docs-layout\"\n      {...props}\n      className={cn(\n        \"min-w-0 [--docs-header-height:0px]\",\n        navEnabled && \"max-md:[--docs-header-height:--spacing(14)]\",\n        // room for the panel that reopens a collapsed sidebar\n        sidebarCollapsible &&\n          \"md:peer-data-[state=collapsed]:[--docs-page-offset:--spacing(10)]\",\n        className,\n      )}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/docs/layouts/docs/slots/container.tsx"
    }
  ],
  "type": "registry:component"
}