{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tree-view",
  "title": "Tree View",
  "description": "An ARIA tree with arrow keys, typeahead, selection and lazy children.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/default/ui/tree-view.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronRightIcon, LoaderIcon } from \"lucide-react\"\n\nimport { cn } from \"@/registry/default/lib/utils\"\n\ntype TreeNode = {\n  id: string\n  label: React.ReactNode\n  textValue?: string\n  icon?: React.ReactNode\n  children?: TreeNode[]\n  isBranch?: boolean\n  disabled?: boolean\n}\n\ntype VisibleNode = {\n  node: TreeNode\n  depth: number\n  parentId: string | null\n  isBranch: boolean\n  position: number\n  siblingCount: number\n}\n\nconst TYPEAHEAD_RESET_MS = 500\n\nfunction flattenVisible(\n  nodes: TreeNode[],\n  expanded: string[],\n  depth = 0,\n  parentId: string | null = null\n): VisibleNode[] {\n  return nodes.flatMap((node, index) => {\n    const isBranch = node.isBranch ?? node.children !== undefined\n    const row = {\n      node,\n      depth,\n      parentId,\n      isBranch,\n      position: index + 1,\n      siblingCount: nodes.length,\n    }\n    if (!isBranch || !expanded.includes(node.id) || !node.children) return [row]\n    return [row, ...flattenVisible(node.children, expanded, depth + 1, node.id)]\n  })\n}\n\nfunction textOf(node: TreeNode) {\n  return (\n    node.textValue ?? (typeof node.label === \"string\" ? node.label : \"\")\n  ).toLowerCase()\n}\n\nfunction TreeView({\n  items,\n  expanded,\n  defaultExpanded = [],\n  onExpandedChange,\n  selected,\n  defaultSelected = null,\n  onSelectedChange,\n  className,\n  ...props\n}: Omit<React.ComponentProps<\"ul\">, \"children\"> & {\n  items: TreeNode[]\n  expanded?: string[]\n  defaultExpanded?: string[]\n  onExpandedChange?: (expanded: string[]) => void\n  selected?: string | null\n  defaultSelected?: string | null\n  onSelectedChange?: (id: string) => void\n}) {\n  const [uncontrolledExpanded, setUncontrolledExpanded] =\n    React.useState(defaultExpanded)\n  const [uncontrolledSelected, setUncontrolledSelected] =\n    React.useState(defaultSelected)\n  const [focusedId, setFocusedId] = React.useState<string | null>(null)\n  const itemRefs = React.useRef(new Map<string, HTMLLIElement>())\n  const typeahead = React.useRef({ query: \"\", lastKeyAt: 0 })\n  const expandedIds = expanded ?? uncontrolledExpanded\n  const selectedId = selected !== undefined ? selected : uncontrolledSelected\n  const visible = flattenVisible(items, expandedIds)\n  const tabStopId = visible.some((row) => row.node.id === focusedId)\n    ? focusedId\n    : (selectedId ?? visible[0]?.node.id)\n\n  function setExpanded(next: string[]) {\n    if (expanded === undefined) setUncontrolledExpanded(next)\n    onExpandedChange?.(next)\n  }\n\n  function toggle(id: string, shouldExpand = !expandedIds.includes(id)) {\n    if (shouldExpand === expandedIds.includes(id)) return\n    setExpanded(\n      shouldExpand\n        ? [...expandedIds, id]\n        : expandedIds.filter((expandedId) => expandedId !== id)\n    )\n  }\n\n  function select(id: string) {\n    if (selected === undefined) setUncontrolledSelected(id)\n    onSelectedChange?.(id)\n  }\n\n  function focusNode(id: string | undefined) {\n    if (!id) return\n    setFocusedId(id)\n    itemRefs.current.get(id)?.focus()\n  }\n\n  function focusByTypeahead(\n    key: string,\n    fromIndex: number,\n    keyPressedAtMs: number\n  ) {\n    const state = typeahead.current\n    state.query =\n      keyPressedAtMs - state.lastKeyAt > TYPEAHEAD_RESET_MS\n        ? key\n        : state.query + key\n    state.lastKeyAt = keyPressedAtMs\n    const searchStart = state.query.length > 1 ? fromIndex : fromIndex + 1\n    const ordered = [\n      ...visible.slice(searchStart),\n      ...visible.slice(0, searchStart),\n    ]\n    focusNode(\n      ordered.find((row) => textOf(row.node).startsWith(state.query))?.node.id\n    )\n  }\n\n  function handleKeyDown(\n    event: React.KeyboardEvent<HTMLLIElement>,\n    index: number\n  ) {\n    const row = visible[index]\n    const isExpanded = expandedIds.includes(row.node.id)\n\n    switch (event.key) {\n      case \"ArrowDown\":\n        focusNode(visible[index + 1]?.node.id)\n        break\n      case \"ArrowUp\":\n        focusNode(visible[index - 1]?.node.id)\n        break\n      case \"ArrowRight\":\n        if (!row.isBranch) return\n        if (!isExpanded) toggle(row.node.id, true)\n        else focusNode(row.node.children?.[0]?.id)\n        break\n      case \"ArrowLeft\":\n        if (row.isBranch && isExpanded) toggle(row.node.id, false)\n        else focusNode(row.parentId ?? undefined)\n        break\n      case \"Home\":\n        focusNode(visible[0]?.node.id)\n        break\n      case \"End\":\n        focusNode(visible.at(-1)?.node.id)\n        break\n      case \"Enter\":\n      case \" \":\n        if (row.node.disabled) return\n        select(row.node.id)\n        if (row.isBranch && event.key === \"Enter\") toggle(row.node.id)\n        break\n      default:\n        if (\n          event.key.length !== 1 ||\n          event.metaKey ||\n          event.ctrlKey ||\n          event.altKey\n        )\n          return\n        focusByTypeahead(event.key.toLowerCase(), index, event.timeStamp)\n    }\n    event.preventDefault()\n  }\n\n  return (\n    <ul\n      role=\"tree\"\n      data-slot=\"tree-view\"\n      className={cn(\"flex flex-col gap-px text-sm\", className)}\n      {...props}\n    >\n      {visible.map((row, index) => {\n        const isExpanded = row.isBranch && expandedIds.includes(row.node.id)\n        const isLoading = isExpanded && row.node.children === undefined\n\n        return (\n          <React.Fragment key={row.node.id}>\n            <li\n              ref={(element) => {\n                if (element) itemRefs.current.set(row.node.id, element)\n                else itemRefs.current.delete(row.node.id)\n              }}\n              role=\"treeitem\"\n              data-slot=\"tree-view-item\"\n              aria-level={row.depth + 1}\n              aria-posinset={row.position}\n              aria-setsize={row.siblingCount}\n              aria-expanded={row.isBranch ? isExpanded : undefined}\n              aria-selected={selectedId === row.node.id}\n              aria-disabled={row.node.disabled || undefined}\n              tabIndex={tabStopId === row.node.id ? 0 : -1}\n              onFocus={() => setFocusedId(row.node.id)}\n              onKeyDown={(event) => handleKeyDown(event, index)}\n              onClick={() => {\n                if (row.node.disabled) return\n                select(row.node.id)\n                if (row.isBranch) toggle(row.node.id)\n              }}\n              style={{ paddingLeft: `calc(${row.depth} * 1rem + 0.5rem)` }}\n              className={cn(\n                \"flex h-8 cursor-default items-center gap-1.5 rounded-md pr-2 outline-none select-none\",\n                \"hover:bg-accent/60 focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n                \"aria-selected:bg-accent aria-selected:font-medium aria-selected:text-accent-foreground\",\n                \"aria-disabled:pointer-events-none aria-disabled:opacity-50\",\n                \"[&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground\"\n              )}\n            >\n              {row.isBranch ? (\n                <ChevronRightIcon\n                  className={cn(\n                    \"transition-transform duration-200\",\n                    isExpanded && \"rotate-90\"\n                  )}\n                />\n              ) : (\n                <span className=\"size-4 shrink-0\" />\n              )}\n              {row.node.icon}\n              <span className=\"truncate\">{row.node.label}</span>\n            </li>\n            {isLoading && (\n              <li\n                role=\"none\"\n                style={{\n                  paddingLeft: `calc(${row.depth + 1} * 1rem + 0.5rem)`,\n                }}\n                className=\"flex h-8 items-center gap-1.5 text-muted-foreground [&_svg]:size-4\"\n              >\n                <LoaderIcon className=\"animate-spin\" />\n                Loading\n              </li>\n            )}\n          </React.Fragment>\n        )\n      })}\n    </ul>\n  )\n}\n\nexport { TreeView, type TreeNode }\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}