{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "layout-shared",
  "title": "Shared layout options",
  "description": "BaseLayoutProps, link items, layout tabs, the AI chat panel and button, the skip link and helpers shared by the docs, notebook and home layouts.",
  "dependencies": [
    "@fuma-translate/react@^1.0.2",
    "cn",
    "fumadocs-core@^16.17.8",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "@docscn/docs-utils",
    "@docscn/theme-switch",
    "@docscn/search-trigger"
  ],
  "files": [
    {
      "path": "registry/base/docs/layouts/shared/index.tsx",
      "content": "// Adapted from Fumadocs UI (https://github.com/fuma-nama/fumadocs)\n// Copyright (c) 2023 Fuma, MIT License\n\nimport { type FC, useMemo, type ComponentProps, type ReactNode } from \"react\";\nimport * as PageTree from \"fumadocs-core/page-tree\";\nimport { isActive, normalize } from \"../../utils/urls\";\nimport type { ThemeSwitchProps } from \"./slots/theme-switch\";\nimport type {\n  FullSearchTriggerProps,\n  SearchTriggerProps,\n} from \"./slots/search-trigger\";\n\nexport interface NavOptions {\n  enabled?: boolean;\n  children?: ReactNode;\n  title?: ReactNode | FC<ComponentProps<\"a\">>;\n\n  /**\n   * Redirect url of title\n   * @defaultValue '/'\n   */\n  url?: string;\n\n  /**\n   * Use transparent background\n   *\n   * @defaultValue none\n   */\n  transparentMode?: \"always\" | \"top\" | \"none\";\n}\n\nexport interface BaseLayoutProps {\n  /**\n   * GitHub url\n   */\n  githubUrl?: string;\n  links?: LinkItemType[];\n  /**\n   * navigation config\n   */\n  nav?: NavOptions;\n  children?: ReactNode;\n  themeSwitch?: ThemeSwitchOptions;\n  searchToggle?: SearchToggleOptions;\n}\n\nexport interface SearchToggleOptions {\n  enabled?: boolean;\n  sm?: SearchTriggerProps;\n  full?: FullSearchTriggerProps;\n}\n\nexport interface ThemeSwitchOptions extends ThemeSwitchProps {\n  enabled?: boolean;\n}\n\nexport interface AIChatOptions {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  /**\n   * The chat panel, placed beside the page on wide viewports, and floating over it on smaller ones.\n   */\n  panel?: ReactNode;\n  /**\n   * Render a floating Ask AI button, for layouts without one of their own (Docs and Notebook)\n   *\n   * @defaultValue false\n   */\n  trigger?: boolean;\n}\n\nexport interface LayoutTab {\n  /**\n   * Redirect URL of the folder, usually the index page\n   */\n  url: string;\n  icon?: ReactNode;\n  title: ReactNode;\n  description?: ReactNode;\n  unlisted?: boolean;\n  props?: ComponentProps<\"a\">;\n\n  /**\n   * bind to a page tree node.\n   */\n  $folder?: PageTree.Folder;\n\n  /**\n   * Detect from a list of urls (when not bound to page tree).\n   */\n  urls?: Set<string>;\n}\n\nexport interface GetLayoutTabsOptions {\n  transform?: (option: LayoutTab, node: PageTree.Folder) => LayoutTab | null;\n}\n\nconst defaultTransform: GetLayoutTabsOptions[\"transform\"] = (option, node) => {\n  if (!node.icon) return option;\n\n  return {\n    ...option,\n    icon: (\n      <div className=\"size-full max-md:rounded-md max-md:border max-md:bg-secondary max-md:p-1.5 [&_svg]:size-full\">\n        {node.icon}\n      </div>\n    ),\n  };\n};\n\nexport function getLayoutTabs(\n  tree: PageTree.Root,\n  { transform = defaultTransform }: GetLayoutTabsOptions = {},\n): LayoutTab[] {\n  const results: LayoutTab[] = [];\n\n  function next(node: PageTree.Root | PageTree.Folder, unlisted?: boolean) {\n    if (\"root\" in node && node.root) {\n      const url =\n        node.index?.url ??\n        node.children.find((node) => node.type === \"page\")?.url;\n\n      if (url) {\n        const option: LayoutTab = {\n          title: node.name,\n          icon: node.icon,\n          description: node.description,\n          url,\n          unlisted,\n          $folder: node,\n        };\n\n        const mapped = transform ? transform(option, node) : option;\n        if (mapped) results.push(mapped);\n      }\n    }\n\n    for (const child of node.children) {\n      if (child.type === \"folder\") next(child, unlisted);\n    }\n  }\n\n  next(tree);\n  if (tree.fallback) next(tree.fallback, true);\n\n  return results;\n}\n\nexport function isLayoutTabActive(\n  tab: LayoutTab,\n  path: PageTree.Node[],\n  pathname: string,\n) {\n  if (tab.$folder) {\n    const folder = tab.$folder;\n    return path.some(\n      (node) =>\n        node.type === \"folder\" && (node.$id === folder.$id || node === folder),\n    );\n  }\n\n  if (tab.urls) {\n    return tab.urls.has(normalize(pathname));\n  }\n\n  return isActive(tab.url, pathname, true);\n}\n\ninterface Filterable {\n  /**\n   * Restrict where the item is displayed\n   *\n   * @defaultValue 'all'\n   */\n  on?: \"menu\" | \"nav\" | \"all\";\n}\n\ninterface WithHref {\n  url: string;\n  /**\n   * When the item is marked as active\n   *\n   * @defaultValue 'url'\n   */\n  active?: \"url\" | \"nested-url\" | \"none\";\n  external?: boolean;\n}\n\nexport interface MainItemType extends WithHref, Filterable {\n  type?: \"main\";\n  icon?: ReactNode;\n  text: ReactNode;\n  description?: ReactNode;\n}\n\nexport interface IconItemType extends WithHref, Filterable {\n  type: \"icon\";\n  /**\n   * `aria-label` of icon button\n   */\n  label?: string;\n  icon: ReactNode;\n  text: ReactNode;\n  /**\n   * @defaultValue true\n   */\n  secondary?: boolean;\n}\n\nexport interface ButtonItemType extends WithHref, Filterable {\n  type: \"button\";\n  icon?: ReactNode;\n  text: ReactNode;\n  /**\n   * @defaultValue false\n   */\n  secondary?: boolean;\n}\n\nexport interface MenuItemType extends Partial<WithHref>, Filterable {\n  type: \"menu\";\n  icon?: ReactNode;\n  text: ReactNode;\n\n  items: (\n    | (MainItemType & {\n        /**\n         * Options when displayed on navigation menu\n         */\n        menu?: ComponentProps<\"a\"> & {\n          banner?: ReactNode;\n        };\n      })\n    | CustomItemType\n  )[];\n\n  /**\n   * @defaultValue false\n   */\n  secondary?: boolean;\n}\n\nexport interface CustomItemType extends Filterable {\n  type: \"custom\";\n  /**\n   * @defaultValue false\n   */\n  secondary?: boolean;\n  children: ReactNode;\n}\n\nexport type LinkItemType =\n  MainItemType | IconItemType | ButtonItemType | MenuItemType | CustomItemType;\n\n/**\n * Get link items with shortcuts\n */\nexport function resolveLinkItems({\n  links = [],\n  githubUrl,\n}: Pick<BaseLayoutProps, \"links\" | \"githubUrl\">): LinkItemType[] {\n  const result = [...links];\n\n  if (githubUrl)\n    result.push({\n      type: \"icon\",\n      url: githubUrl,\n      text: \"Github\",\n      label: \"GitHub\",\n      icon: (\n        <svg aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n          <path d=\"M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12\" />\n        </svg>\n      ),\n      external: true,\n    });\n\n  return result;\n}\n\nexport interface GroupedLinkItems {\n  all: LinkItemType[];\n  navItems: LinkItemType[];\n  menuItems: LinkItemType[];\n}\n\nexport function useLinkItems({\n  githubUrl,\n  links,\n}: Pick<BaseLayoutProps, \"links\" | \"githubUrl\">): GroupedLinkItems {\n  return useMemo(() => {\n    const all = resolveLinkItems({ links, githubUrl });\n    const navItems: LinkItemType[] = [];\n    const menuItems: LinkItemType[] = [];\n\n    for (const item of all) {\n      switch (item.on) {\n        case \"menu\":\n          menuItems.push(item);\n          break;\n        case \"nav\":\n          navItems.push(item);\n          break;\n        default:\n          navItems.push(item);\n          menuItems.push(item);\n      }\n    }\n\n    return { navItems, menuItems, all };\n  }, [links, githubUrl]);\n}\n\nexport function isLinkItemActive(link: LinkItemType, pathname: string) {\n  if (link.type === \"custom\" || !link.url) return false;\n  if (link.active === \"none\") return false;\n\n  return isActive(link.url, pathname, link.active === \"nested-url\");\n}\n\nexport { LinkItem, NavTitle } from \"./client\";\n",
      "type": "registry:component",
      "target": "@components/docs/layouts/shared/index.tsx"
    },
    {
      "path": "registry/base/docs/layouts/shared/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 { usePathname } from \"fumadocs-core/framework\";\nimport Link from \"fumadocs-core/link\";\nimport { useTranslations } from \"@fuma-translate/react\";\nimport { cn } from \"cn\";\nimport { MessageCircleIcon } from \"lucide-react\";\nimport { type ComponentProps, type ReactNode, useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  type AIChatOptions,\n  isLinkItemActive,\n  type LinkItemType,\n  type NavOptions,\n} from \".\";\n\nexport function LinkItem({\n  ref,\n  item,\n  ...props\n}: Omit<ComponentProps<\"a\">, \"href\"> & {\n  item: Extract<LinkItemType, { url: string }>;\n}) {\n  const pathname = usePathname();\n  const active = isLinkItemActive(item, pathname);\n\n  return (\n    <Link\n      ref={ref}\n      href={item.url}\n      external={item.external}\n      {...props}\n      data-active={active}\n    >\n      {props.children}\n    </Link>\n  );\n}\n\n/**\n * The title of the navigation (`nav.title`), linking to `nav.url`.\n */\nexport function NavTitle({\n  nav = {},\n  href: defaultUrl = \"/\",\n  ...props\n}: ComponentProps<\"a\"> & { nav?: NavOptions }) {\n  const { url = defaultUrl, title } = nav;\n\n  if (typeof title === \"function\") return title({ href: url, ...props });\n  return (\n    <Link href={url} {...props}>\n      {title}\n    </Link>\n  );\n}\n\n/**\n * The place of AI chat, docked by the layout on wide viewports, and floating on smaller ones.\n */\nexport function AIChatPanel({\n  open,\n  className,\n  children,\n}: {\n  open: boolean;\n  className?: string;\n  children: ReactNode;\n}) {\n  // mount the chat once opened\n  const [mounted, setMounted] = useState(open);\n  if (open && !mounted) setMounted(true);\n\n  return (\n    <aside\n      data-state={open ? \"open\" : \"closed\"}\n      className={cn(\n        \"z-40 shrink-0 overflow-clip bg-card text-card-foreground transition-[width,translate] duration-300 ease-in-out [--ai-chat-width:min(--spacing(100),100vw---spacing(4))] motion-reduce:transition-none max-xl:fixed max-xl:inset-y-2 max-xl:end-2 max-xl:w-(--ai-chat-width) max-xl:rounded-xl max-xl:border max-xl:shadow-xl\",\n        open\n          ? \"w-(--ai-chat-width)\"\n          : // visible at once when opened, so it can take focus; hidden only after closing\n            \"invisible w-0 transition-[width,translate,visibility] max-xl:translate-x-[calc(100%+--spacing(2))] rtl:max-xl:-translate-x-[calc(100%+--spacing(2))]\",\n        className,\n      )}\n    >\n      {/* fixed width while resizing */}\n      <div className=\"flex size-full flex-col xl:w-(--ai-chat-width)\">\n        {mounted && children}\n      </div>\n    </aside>\n  );\n}\n\n/** a floating button toggling the chat, for layouts without one of their own */\nexport function AIChatTrigger({ open, onOpenChange }: AIChatOptions) {\n  const t = useTranslations({ note: \"AI chat button\" });\n\n  return (\n    <Button\n      variant=\"secondary\"\n      aria-expanded={open}\n      className={cn(\n        \"fixed inset-e-4 bottom-4 z-20 gap-2 rounded-2xl text-muted-foreground shadow-lg transition-[translate,opacity] motion-reduce:transition-none\",\n        open && \"translate-y-10 opacity-0\",\n      )}\n      inert={open}\n      onClick={() => onOpenChange(!open)}\n    >\n      <MessageCircleIcon aria-hidden className=\"size-4.5\" />\n      {t(\"Ask AI\")}\n    </Button>\n  );\n}\n\nexport function SkipToContent() {\n  const t = useTranslations({ note: \"skip link\" });\n\n  return (\n    <a href=\"#nd-page\" className=\"sr-only\">\n      {t(\"Skip to Content\")}\n    </a>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/docs/layouts/shared/client.tsx"
    }
  ],
  "type": "registry:component"
}