{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tree-context",
  "title": "Page tree context",
  "description": "TreeContextProvider and hooks for the current page tree, the path to the current page, layout tab groups and the linear page list used for previous/next links.",
  "dependencies": [
    "fumadocs-core@^16.17.8"
  ],
  "registryDependencies": [
    "@docscn/layout-shared",
    "@docscn/docs-utils"
  ],
  "files": [
    {
      "path": "registry/base/docs/contexts/tree.tsx",
      "content": "// Adapted from Fumadocs UI (https://github.com/fuma-nama/fumadocs)\n// Copyright (c) 2023 Fuma, MIT License\n\"use client\";\nimport * as PageTree from \"fumadocs-core/page-tree\";\nimport { usePathname } from \"fumadocs-core/framework\";\nimport { type ReactNode, useMemo, useRef, createContext, use } from \"react\";\nimport { searchPath } from \"fumadocs-core/breadcrumb\";\nimport type { LayoutTab } from \"../layouts/shared\";\nimport { findLast } from \"../utils/array\";\n\ntype MakeRequired<O, K extends keyof O> = Omit<O, K> & Pick<Required<O>, K>;\n\ninterface TreeContextType {\n  root: MakeRequired<PageTree.Root | PageTree.Folder, \"$id\">;\n  full: PageTree.Root;\n}\n\nconst TreeContext = createContext<TreeContextType | null>(null);\nconst PathContext = createContext<PageTree.Node[]>([]);\n\nexport function TreeContextProvider({\n  tree: rawTree,\n  children,\n}: {\n  tree: PageTree.Root;\n  children: ReactNode;\n}) {\n  const nextIdRef = useRef(0);\n  const pathname = usePathname();\n\n  // I found that object-typed props passed from a RSC will be re-constructed, hence breaking all hooks' dependencies\n  // using the id here to make sure this never happens\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  const tree = useMemo(() => rawTree, [rawTree.$id]);\n  const path = useMemo(() => {\n    return (\n      searchPath(tree.children, pathname) ??\n      (tree.fallback ? searchPath(tree.fallback.children, pathname) : null) ??\n      []\n    );\n  }, [tree, pathname]);\n\n  const root =\n    findLast(path, (item) => item.type === \"folder\" && item.root) ?? tree;\n  root.$id ??= String(nextIdRef.current++);\n\n  return (\n    <TreeContext\n      value={useMemo(\n        () => ({ root, full: tree }) as TreeContextType,\n        [root, tree],\n      )}\n    >\n      <PathContext value={path}>{children}</PathContext>\n    </TreeContext>\n  );\n}\n\nexport function useTreePath(): PageTree.Node[] {\n  return use(PathContext);\n}\n\nexport function useTreeContext(): TreeContextType {\n  const ctx = use(TreeContext);\n\n  if (!ctx)\n    throw new Error(\"You must wrap this component under <DocsLayout />\");\n  return ctx;\n}\n\nexport interface TabsGroup {\n  /** the root folder on the current page's path, undefined for tabs not bound to the page tree */\n  active?: PageTree.Folder;\n  options: LayoutTab[];\n}\n\n/**\n * Group tabs by the root folders on the current page's path (outermost first): each tab is\n * matched to its root folder, linking to the projection of current page when possible.\n *\n * Tabs not bound to the page tree are appended to the group of `root: true` folders.\n */\nexport function useTabsGroups(tabs: LayoutTab[]): TabsGroup[] {\n  const { full: tree } = useTreeContext();\n  const path = use(PathContext);\n\n  return useMemo(() => {\n    const out: TabsGroup[] = [];\n    const last = path[path.length - 1];\n    const page = last?.type === \"page\" ? last : undefined;\n    // the fallback tree shares the same scope as the main tree\n    let scope = tree.fallback\n      ? [...tree.children, ...tree.fallback.children]\n      : tree.children;\n\n    for (const node of path) {\n      if (node.type !== \"folder\" || !node.root) continue;\n      const group: TabsGroup = { active: node, options: [] };\n      collectTabs(scope, node, page, tabs, group.options);\n      if (group.options.length > 0) out.push(group);\n      scope = node.children;\n    }\n\n    const custom = tabs.filter((tab) => !tab.$folder);\n    if (custom.length > 0) {\n      const group = findLast(out, (group) => group.active?.root === true);\n      if (group) group.options.push(...custom);\n      else out.push({ options: custom });\n    }\n\n    return out;\n  }, [tabs, tree, path]);\n}\n\n/** collect the tabs of root folders with the same type as `active` within a scope */\nfunction collectTabs(\n  scope: PageTree.Node[],\n  active: PageTree.Folder,\n  page: PageTree.Item | undefined,\n  tabs: LayoutTab[],\n  out: LayoutTab[],\n) {\n  for (const node of scope) {\n    if (node.type !== \"folder\") continue;\n    if (node.root === active.root) {\n      const tab = tabs.find(\n        (tab) =>\n          tab.$folder && (tab.$folder === node || tab.$folder.$id === node.$id),\n      );\n      if (!tab) continue;\n\n      const projection = page && PageTree.findProjection(active, node, page);\n      out.push(projection ? { ...tab, url: projection.url } : tab);\n    } else if (!node.root) {\n      // any other root folder starts a new scope\n      collectTabs(node.children, active, page, tabs, out);\n    }\n  }\n}\n",
      "type": "registry:component",
      "target": "@components/docs/contexts/tree.tsx"
    },
    {
      "path": "registry/base/docs/utils/use-footer-items.ts",
      "content": "// Adapted from Fumadocs UI (https://github.com/fuma-nama/fumadocs)\n// Copyright (c) 2023 Fuma, MIT License\n\"use client\";\nimport { useTreeContext } from \"../contexts/tree\";\nimport type * as PageTree from \"fumadocs-core/page-tree\";\n\nconst footerCache = new WeakMap<\n  PageTree.Root | PageTree.Folder,\n  PageTree.Item[]\n>();\n\n/**\n * @returns a list of page tree items (linear), that you can obtain footer items\n */\nexport function useFooterItems(): PageTree.Item[] {\n  const { root } = useTreeContext();\n  const cached = footerCache.get(root);\n  if (cached) return cached;\n\n  const list: PageTree.Item[] = [];\n  function onNode(node: PageTree.Node) {\n    if (node.type === \"folder\") {\n      if (node.index) onNode(node.index);\n      for (const child of node.children) onNode(child);\n    } else if (node.type === \"page\" && !node.external) {\n      list.push(node);\n    }\n  }\n\n  for (const child of root.children) onNode(child);\n  footerCache.set(root, list);\n  return list;\n}\n",
      "type": "registry:lib",
      "target": "@components/docs/utils/use-footer-items.ts"
    }
  ],
  "type": "registry:component"
}