{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "async-tree",
  "title": "Async Tree",
  "description": "A tree that loads each branch the first time it opens, with a busy row, a spoken status, an error state and a retry.",
  "registryDependencies": [
    "@tealmac/tree-view"
  ],
  "files": [
    {
      "path": "registry/tealmac/async-tree.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n  findNode,\n  TreeView,\n  updateNode,\n  useControllableState,\n  type TreeNode,\n  type TreeRow,\n  type TreeViewProps,\n} from \"@/components/ui/tree-view\"\n\nexport interface AsyncTreeProps<T = unknown> extends Omit<\n  TreeViewProps<T>,\n  \"nodes\" | \"isRowLoading\"\n> {\n  /** The top level. Mark a branch whose children load later with `hasChildren: true`. */\n  nodes: TreeNode<T>[]\n  /** Called the first time a branch opens. Resolve with its children. */\n  loadChildren: (node: TreeNode<T>) => Promise<TreeNode<T>[]>\n  /** Called with the full tree each time children arrive. */\n  onNodesChange?: (nodes: TreeNode<T>[]) => void\n  /** Shown at the end of a row whose load failed. */\n  retryLabel?: string\n}\n\ntype LoadState = \"loading\" | \"error\"\n\n/**\n * A tree that loads each branch when it first opens. The row shows a spinner\n * and `aria-busy` while it loads, a failed load keeps the branch closed and\n * offers Retry (Enter on the row works too), and every change is announced.\n */\nexport function AsyncTree<T = unknown>({\n  nodes: initial,\n  loadChildren,\n  onNodesChange,\n  retryLabel = \"Retry\",\n  expanded,\n  defaultExpanded,\n  onExpandedChange,\n  onActivate,\n  renderTrailing,\n  ...props\n}: AsyncTreeProps<T>) {\n  const [nodes, setNodes] = React.useState(initial)\n  const [status, setStatus] = React.useState<Record<string, LoadState>>({})\n  const [announcement, setAnnouncement] = React.useState(\"\")\n  const [open, setOpen] = useControllableState(\n    expanded,\n    defaultExpanded ?? [],\n    onExpandedChange\n  )\n  const nodesRef = React.useRef(nodes)\n  nodesRef.current = nodes\n  const inFlight = React.useRef(new Set<string>())\n\n  const load = React.useCallback(\n    async (id: string) => {\n      const node = findNode(nodesRef.current, id)\n      if (!node || inFlight.current.has(id)) return\n      inFlight.current.add(id)\n      setStatus((s) => ({ ...s, [id]: \"loading\" }))\n      setAnnouncement(`Loading ${node.label}`)\n      try {\n        const children = await loadChildren(node)\n        const next = updateNode(nodesRef.current, id, (n) => ({\n          ...n,\n          hasChildren: false,\n          children,\n        }))\n        nodesRef.current = next\n        setNodes(next)\n        onNodesChange?.(next)\n        setStatus((s) => {\n          const rest = { ...s }\n          delete rest[id]\n          return rest\n        })\n        setAnnouncement(\n          children.length === 0\n            ? `${node.label} is empty`\n            : `Loaded ${children.length} ${children.length === 1 ? \"item\" : \"items\"} in ${node.label}`\n        )\n      } catch {\n        setStatus((s) => ({ ...s, [id]: \"error\" }))\n        setOpen(open.filter((x) => x !== id))\n        setAnnouncement(`Could not load ${node.label}`)\n      } finally {\n        inFlight.current.delete(id)\n      }\n    },\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [loadChildren, onNodesChange, open]\n  )\n\n  const handleExpanded = (ids: string[]) => {\n    setOpen(ids)\n    for (const id of ids) {\n      if (open.includes(id)) continue\n      const node = findNode(nodesRef.current, id)\n      if (node?.hasChildren && !node.children?.length) void load(id)\n    }\n  }\n\n  // A branch that opens by default loads on mount.\n  React.useEffect(() => {\n    for (const id of open) {\n      const node = findNode(nodesRef.current, id)\n      if (node?.hasChildren && !node.children?.length) void load(id)\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [])\n\n  return (\n    <>\n      <TreeView<T>\n        {...props}\n        nodes={nodes}\n        expanded={open}\n        onExpandedChange={handleExpanded}\n        isRowLoading={(row) => status[row.id] === \"loading\"}\n        onActivate={(node) => {\n          if (status[node.id] === \"error\") {\n            setOpen([...open, node.id])\n            void load(node.id)\n          } else return onActivate ? onActivate(node) : false\n        }}\n        renderTrailing={(row: TreeRow<T>) =>\n          status[row.id] === \"error\" ? (\n            <span className=\"text-destructive flex items-center gap-1.5 text-xs\">\n              Could not load\n              <button\n                type=\"button\"\n                tabIndex={-1}\n                aria-label={`${retryLabel} loading ${row.node.label}`}\n                className={cn(\n                  \"hover:bg-destructive/10 focus-visible:ring-ring/60 rounded px-1.5 py-0.5 font-medium underline outline-none focus-visible:ring-2\"\n                )}\n                onClick={(event) => {\n                  event.stopPropagation()\n                  setOpen([...open, row.id])\n                  void load(row.id)\n                }}\n              >\n                {retryLabel}\n              </button>\n            </span>\n          ) : (\n            renderTrailing?.(row)\n          )\n        }\n      />\n      <div role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n        {announcement}\n      </div>\n    </>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}