{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tree-view",
  "title": "Tree View",
  "description": "An ARIA tree with roving focus, arrow-key navigation, typeahead, single or multiple selection and a headless hook, in one file.",
  "files": [
    {
      "path": "registry/tealmac/tree-view.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronDown, ChevronRight, Loader2 } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* -------------------------------------------------------------------------- */\n/*  Data model and pure helpers                                               */\n/*  Everything below `useTree` is plain TypeScript with no React, so the       */\n/*  other tree components (checkbox tree, sortable tree, transfer tree ...)    */\n/*  and your own code can share it.                                           */\n/* -------------------------------------------------------------------------- */\n\nexport interface TreeNode<T = unknown> {\n  /** Unique across the whole tree. */\n  id: string\n  /** Plain text. Used for typeahead, filtering and screen readers. */\n  label: string\n  children?: TreeNode<T>[]\n  /** A branch whose children are not loaded yet: shows an expander. */\n  hasChildren?: boolean\n  /** Focusable and visible, but cannot be selected, checked or moved. */\n  disabled?: boolean\n  icon?: React.ReactNode\n  /** Renders the row as a link (see TreeNav). */\n  href?: string\n  data?: T\n}\n\nexport interface TreeRow<T = unknown> {\n  node: TreeNode<T>\n  id: string\n  /** 1-based depth, as used by aria-level. */\n  level: number\n  /** Index in the visible list. */\n  index: number\n  parentId: string | null\n  /** 1-based position among its siblings, as used by aria-posinset. */\n  posInSet: number\n  /** Number of siblings, as used by aria-setsize. */\n  setSize: number\n  expanded: boolean\n  expandable: boolean\n}\n\nexport function isBranch(node: {\n  children?: unknown[]\n  hasChildren?: boolean\n}) {\n  return (node.children?.length ?? 0) > 0 || node.hasChildren === true\n}\n\n/** The visible rows, in order: a branch contributes its children only when open. */\nexport function flattenTree<T>(\n  nodes: TreeNode<T>[],\n  expanded: ReadonlySet<string>,\n  level = 1,\n  parentId: string | null = null,\n  out: TreeRow<T>[] = []\n): TreeRow<T>[] {\n  nodes.forEach((node, i) => {\n    const expandable = isBranch(node)\n    const open = expandable && expanded.has(node.id)\n    out.push({\n      node,\n      id: node.id,\n      level,\n      index: out.length,\n      parentId,\n      posInSet: i + 1,\n      setSize: nodes.length,\n      expanded: open,\n      expandable,\n    })\n    if (open && node.children?.length) {\n      flattenTree(node.children, expanded, level + 1, node.id, out)\n    }\n  })\n  return out\n}\n\nexport function walkTree<T>(\n  nodes: TreeNode<T>[],\n  visit: (node: TreeNode<T>, parent: TreeNode<T> | null, depth: number) => void,\n  parent: TreeNode<T> | null = null,\n  depth = 0\n) {\n  for (const node of nodes) {\n    visit(node, parent, depth)\n    if (node.children) walkTree(node.children, visit, node, depth + 1)\n  }\n}\n\nexport function findNode<T>(\n  nodes: TreeNode<T>[],\n  id: string\n): TreeNode<T> | undefined {\n  for (const node of nodes) {\n    if (node.id === id) return node\n    const found = node.children && findNode(node.children, id)\n    if (found) return found\n  }\n  return undefined\n}\n\n/** The nodes from the root down to `id`, or null when it is not in the tree. */\nexport function findPath<T>(\n  nodes: TreeNode<T>[],\n  id: string\n): TreeNode<T>[] | null {\n  for (const node of nodes) {\n    if (node.id === id) return [node]\n    const rest = node.children && findPath(node.children, id)\n    if (rest) return [node, ...rest]\n  }\n  return null\n}\n\nexport function getParentId<T>(nodes: TreeNode<T>[], id: string) {\n  const path = findPath(nodes, id)\n  if (!path) return undefined\n  return path.length > 1 ? path[path.length - 2].id : null\n}\n\n/** The id of `node` and every node below it. */\nexport function collectIds<T>(node: TreeNode<T>, includeSelf = true) {\n  const ids: string[] = []\n  walkTree([node], (n) => {\n    if (includeSelf || n !== node) ids.push(n.id)\n  })\n  return ids\n}\n\n/** Ids of every node that has children. */\nexport function collectBranchIds<T>(nodes: TreeNode<T>[]) {\n  const ids: string[] = []\n  walkTree(nodes, (n) => {\n    if (isBranch(n)) ids.push(n.id)\n  })\n  return ids\n}\n\n/** Replace the children of `parentId` (null: the roots) with `update(children)`. */\nexport function updateChildren<T>(\n  nodes: TreeNode<T>[],\n  parentId: string | null,\n  update: (children: TreeNode<T>[]) => TreeNode<T>[]\n): TreeNode<T>[] {\n  if (parentId === null) return update(nodes)\n  return nodes.map((node) => {\n    if (node.id === parentId)\n      return { ...node, children: update(node.children ?? []) }\n    if (!node.children) return node\n    const next = updateChildren(node.children, parentId, update)\n    return next === node.children ? node : { ...node, children: next }\n  })\n}\n\n/** Replace one node. Returns the same array when the id is not found. */\nexport function updateNode<T>(\n  nodes: TreeNode<T>[],\n  id: string,\n  update: (node: TreeNode<T>) => TreeNode<T>\n): TreeNode<T>[] {\n  let changed = false\n  const walk = (list: TreeNode<T>[]): TreeNode<T>[] =>\n    list.map((node) => {\n      if (node.id === id) {\n        changed = true\n        return update(node)\n      }\n      if (!node.children) return node\n      const next = walk(node.children)\n      return next.some((child, i) => child !== node.children![i])\n        ? { ...node, children: next }\n        : node\n    })\n  const result = walk(nodes)\n  return changed ? result : nodes\n}\n\nexport function removeNode<T>(\n  nodes: TreeNode<T>[],\n  id: string\n): { nodes: TreeNode<T>[]; removed: TreeNode<T> | null } {\n  let removed: TreeNode<T> | null = null\n  const walk = (list: TreeNode<T>[]): TreeNode<T>[] => {\n    const out: TreeNode<T>[] = []\n    for (const node of list) {\n      if (node.id === id) {\n        removed = node\n        continue\n      }\n      if (node.children) {\n        const next = walk(node.children)\n        const same =\n          next.length === node.children.length &&\n          next.every((child, i) => child === node.children![i])\n        out.push(same ? node : { ...node, children: next })\n      } else out.push(node)\n    }\n    return out\n  }\n  const result = walk(nodes)\n  return { nodes: removed ? result : nodes, removed }\n}\n\nexport type DropPosition = \"before\" | \"after\" | \"inside\"\n\n/**\n * Move `id` next to or into `targetId`. Returns the same array when the move\n * is not allowed: onto itself, into its own subtree, or an unknown id.\n */\nexport function moveNode<T>(\n  nodes: TreeNode<T>[],\n  id: string,\n  targetId: string,\n  position: DropPosition\n): TreeNode<T>[] {\n  if (id === targetId) return nodes\n  const source = findNode(nodes, id)\n  if (!source || !findNode(nodes, targetId)) return nodes\n  if (findNode(source.children ?? [], targetId)) return nodes\n  const { nodes: without, removed } = removeNode(nodes, id)\n  if (!removed) return nodes\n  if (position === \"inside\") {\n    return updateNode(without, targetId, (target) => ({\n      ...target,\n      hasChildren: undefined,\n      children: [...(target.children ?? []), removed],\n    }))\n  }\n  const parentId = getParentId(without, targetId)\n  if (parentId === undefined) return nodes\n  return updateChildren(without, parentId, (siblings) => {\n    const at = siblings.findIndex((s) => s.id === targetId)\n    const copy = siblings.slice()\n    copy.splice(position === \"before\" ? at : at + 1, 0, removed)\n    return copy\n  })\n}\n\n/** Swap with the previous (-1) or next (1) sibling. */\nexport function shiftNode<T>(nodes: TreeNode<T>[], id: string, delta: -1 | 1) {\n  const parentId = getParentId(nodes, id)\n  if (parentId === undefined) return nodes\n  return updateChildren(nodes, parentId, (siblings) => {\n    const at = siblings.findIndex((s) => s.id === id)\n    const to = at + delta\n    if (at < 0 || to < 0 || to >= siblings.length) return siblings\n    const copy = siblings.slice()\n    ;[copy[at], copy[to]] = [copy[to], copy[at]]\n    return copy\n  })\n}\n\n/** Make the node the last child of its previous sibling. */\nexport function indentNode<T>(nodes: TreeNode<T>[], id: string) {\n  const parentId = getParentId(nodes, id)\n  if (parentId === undefined) return nodes\n  return updateChildren(nodes, parentId, (siblings) => {\n    const at = siblings.findIndex((s) => s.id === id)\n    if (at <= 0) return siblings\n    const prev = siblings[at - 1]\n    const node = siblings[at]\n    const copy = siblings.slice()\n    copy.splice(at - 1, 2, {\n      ...prev,\n      hasChildren: undefined,\n      children: [...(prev.children ?? []), node],\n    })\n    return copy\n  })\n}\n\n/** Make the node a sibling placed right after its parent. */\nexport function outdentNode<T>(nodes: TreeNode<T>[], id: string) {\n  const parentId = getParentId(nodes, id)\n  if (!parentId) return nodes\n  const grandId = getParentId(nodes, parentId)\n  if (grandId === undefined) return nodes\n  const node = findNode(nodes, id)!\n  return updateChildren(nodes, grandId, (siblings) =>\n    siblings.flatMap((s) =>\n      s.id === parentId\n        ? [\n            { ...s, children: (s.children ?? []).filter((c) => c.id !== id) },\n            node,\n          ]\n        : [s]\n    )\n  )\n}\n\n/**\n * Keep the leaves that pass `keep` and the branches that still have a leaf.\n * Used to split one tree between two panes.\n */\nexport function pruneTree<T>(\n  nodes: TreeNode<T>[],\n  keep: (leaf: TreeNode<T>) => boolean\n): TreeNode<T>[] {\n  const out: TreeNode<T>[] = []\n  for (const node of nodes) {\n    if (node.children?.length) {\n      const kids = pruneTree(node.children, keep)\n      if (kids.length) out.push({ ...node, children: kids })\n    } else if (keep(node)) out.push(node)\n  }\n  return out\n}\n\n/** The leaves (nodes without children) in document order. */\nexport function collectLeaves<T>(nodes: TreeNode<T>[]) {\n  const leaves: TreeNode<T>[] = []\n  walkTree(nodes, (n) => {\n    if (!n.children?.length) leaves.push(n)\n  })\n  return leaves\n}\n\nfunction fold(text: string) {\n  return text.normalize(\"NFD\").replace(/\\p{M}/gu, \"\").toLowerCase()\n}\n\n/** Case and accent insensitive substring test. */\nexport function matchesQuery(label: string, query: string) {\n  const q = fold(query.trim())\n  return q === \"\" || fold(label).includes(q)\n}\n\nexport interface FilteredTree<T = unknown> {\n  nodes: TreeNode<T>[]\n  /** Branches that must be open to show a match inside them. */\n  expandedIds: string[]\n  /** Nodes whose own label matched. */\n  matchIds: Set<string>\n}\n\n/** Keep the nodes that match and the branches that lead to a match. */\nexport function filterTree<T>(\n  nodes: TreeNode<T>[],\n  query: string,\n  match: (node: TreeNode<T>, query: string) => boolean = (n, q) =>\n    matchesQuery(n.label, q)\n): FilteredTree<T> {\n  const matchIds = new Set<string>()\n  const expandedIds: string[] = []\n  if (query.trim() === \"\") return { nodes, expandedIds, matchIds }\n  const walk = (list: TreeNode<T>[]): TreeNode<T>[] => {\n    const out: TreeNode<T>[] = []\n    for (const node of list) {\n      const self = match(node, query)\n      const kids = node.children ? walk(node.children) : []\n      if (self) matchIds.add(node.id)\n      if (self && kids.length === 0) {\n        out.push(node)\n      } else if (kids.length > 0) {\n        expandedIds.push(node.id)\n        out.push({ ...node, children: kids })\n      }\n    }\n    return out\n  }\n  return { nodes: walk(nodes), expandedIds, matchIds }\n}\n\nexport type CheckState = boolean | \"mixed\"\n\n/** Leaves count themselves; a branch is derived from its leaves. */\nexport function computeCheckStates<T>(\n  nodes: TreeNode<T>[],\n  checked: ReadonlySet<string>,\n  cascade = true\n) {\n  const states = new Map<string, CheckState>()\n  const walk = (node: TreeNode<T>): CheckState => {\n    const kids = node.children ?? []\n    if (!cascade || kids.length === 0) {\n      const own = checked.has(node.id)\n      states.set(node.id, own)\n      kids.forEach(walk)\n      return own\n    }\n    const child = kids.map(walk)\n    const state: CheckState = child.every((s) => s === true)\n      ? true\n      : child.every((s) => s === false)\n        ? false\n        : \"mixed\"\n    states.set(node.id, state)\n    return state\n  }\n  nodes.forEach(walk)\n  return states\n}\n\n/**\n * The next set of checked ids after toggling `id`. With `cascade` a branch\n * toggles every enabled leaf below it and the result lists leaves only.\n */\nexport function toggleChecked<T>(\n  nodes: TreeNode<T>[],\n  checked: ReadonlySet<string>,\n  id: string,\n  cascade = true\n): string[] {\n  const node = findNode(nodes, id)\n  if (!node || node.disabled) return [...checked]\n  const next = new Set(checked)\n  if (!cascade || !node.children?.length) {\n    if (next.has(id)) next.delete(id)\n    else next.add(id)\n    return [...next]\n  }\n  const state = computeCheckStates(nodes, checked, true).get(id)\n  const leaves: string[] = []\n  walkTree([node], (n) => {\n    if (!n.children?.length && !n.disabled) leaves.push(n.id)\n  })\n  for (const leaf of leaves) {\n    if (state === true) next.delete(leaf)\n    else next.add(leaf)\n  }\n  return [...next]\n}\n\n/** First visible row after `from` whose label starts with `buffer`, wrapping round. */\nexport function findTypeaheadMatch<T>(\n  rows: TreeRow<T>[],\n  from: number,\n  buffer: string\n) {\n  if (!buffer || rows.length === 0) return -1\n  const needle = fold(buffer)\n  // The same letter typed again steps to the next row with that initial.\n  const repeated = [...needle].every((c) => c === needle[0])\n  const key = repeated ? needle[0] : needle\n  for (\n    let step = repeated ? 1 : 0;\n    step < rows.length + (repeated ? 1 : 0);\n    step++\n  ) {\n    const at = (from + step) % rows.length\n    if (fold(rows[at].node.label).startsWith(key)) return at\n  }\n  return -1\n}\n\n/* -------------------------------------------------------------------------- */\n/*  Controller hook                                                           */\n/* -------------------------------------------------------------------------- */\n\nexport function useControllableState<V>(\n  value: V | undefined,\n  defaultValue: V,\n  onChange?: (value: V) => void\n) {\n  const [inner, setInner] = React.useState(defaultValue)\n  const controlled = value !== undefined\n  const current = controlled ? value : inner\n  const ref = React.useRef(current)\n  ref.current = current\n  const set = React.useCallback(\n    (next: V) => {\n      ref.current = next\n      if (!controlled) setInner(next)\n      onChange?.(next)\n    },\n    [controlled, onChange]\n  )\n  return [current, set, ref] as const\n}\n\nexport type TreeSelectionMode = \"none\" | \"single\" | \"multiple\"\n\nexport interface UseTreeOptions<T = unknown> {\n  nodes: TreeNode<T>[]\n  expanded?: string[]\n  defaultExpanded?: string[]\n  onExpandedChange?: (ids: string[]) => void\n  selected?: string[]\n  defaultSelected?: string[]\n  onSelectedChange?: (ids: string[]) => void\n  /** Default \"single\". */\n  selectionMode?: TreeSelectionMode\n  /**\n   * How a click or Space changes a multiple selection. \"replace\" (default)\n   * selects only that row, and Ctrl/Cmd or Shift extend it, as in a file\n   * manager. \"toggle\" adds or removes the row, which also works by touch.\n   */\n  selectionBehavior?: \"replace\" | \"toggle\"\n  /**\n   * Enter, or double click. Return false to fall back to the default: a branch\n   * opens or closes, a leaf (or a link) is selected or followed. Without this\n   * prop the default always applies.\n   */\n  onActivate?: (node: TreeNode<T>) => boolean | void\n  /**\n   * Replaces selection for click and Space (the checkbox tree toggles a check\n   * here). Called for enabled rows only.\n   */\n  onItemToggle?: (node: TreeNode<T>, row: TreeRow<T>) => void\n  /**\n   * Rows that return false cannot be selected: Space, Enter and a click open\n   * or close them instead (a tree select that only accepts leaves).\n   */\n  isSelectable?: (node: TreeNode<T>) => boolean\n  /** A virtualised list calls its virtualizer here so the row exists before focus. */\n  scrollToIndex?: (index: number) => void\n  /** Override the detected reading direction (arrow keys swap in rtl). */\n  dir?: \"ltr\" | \"rtl\"\n  /** Row ids that start focused. */\n  defaultFocusedId?: string\n  /** Called when a row gets focus. */\n  onFocusedChange?: (id: string) => void\n  /** Called for keys the tree does not handle, with the focused row. */\n  onRowKeyDown?: (event: React.KeyboardEvent, row: TreeRow<T>) => void\n}\n\nexport function useTree<T = unknown>(options: UseTreeOptions<T>) {\n  const {\n    nodes,\n    selectionMode = \"single\",\n    selectionBehavior = \"replace\",\n    onActivate,\n    onItemToggle,\n    isSelectable,\n    scrollToIndex,\n    dir,\n    onRowKeyDown,\n    onFocusedChange,\n  } = options\n  const [expandedList, setExpandedList] = useControllableState(\n    options.expanded,\n    options.defaultExpanded ?? [],\n    options.onExpandedChange\n  )\n  const [selectedList, setSelectedList] = useControllableState(\n    options.selected,\n    options.defaultSelected ?? [],\n    options.onSelectedChange\n  )\n  const expandedSet = React.useMemo(() => new Set(expandedList), [expandedList])\n  const selectedSet = React.useMemo(() => new Set(selectedList), [selectedList])\n  const rows = React.useMemo(\n    () => flattenTree(nodes, expandedSet),\n    [nodes, expandedSet]\n  )\n  const rowsRef = React.useRef(rows)\n  rowsRef.current = rows\n\n  const rootRef = React.useRef<HTMLDivElement | null>(null)\n  const [focusedId, setFocusedId] = React.useState<string | null>(\n    options.defaultFocusedId ?? null\n  )\n  const pendingFocus = React.useRef<string | null>(null)\n  const anchorRef = React.useRef<string | null>(null)\n  const typeahead = React.useRef({ buffer: \"\", timer: 0 as unknown as number })\n\n  const tabbableId =\n    focusedId && rows.some((r) => r.id === focusedId)\n      ? focusedId\n      : (rows.find((r) => !r.node.disabled)?.id ?? rows[0]?.id ?? null)\n\n  // Move DOM focus once the row exists (a virtualised row may mount late).\n  React.useEffect(() => {\n    const id = pendingFocus.current\n    if (!id || !rootRef.current) return\n    const el = Array.from(\n      rootRef.current.querySelectorAll<HTMLElement>(\"[data-tree-id]\")\n    ).find((item) => item.dataset.treeId === id)\n    if (el) {\n      pendingFocus.current = null\n      el.focus()\n    }\n  })\n\n  React.useEffect(() => () => window.clearTimeout(typeahead.current.timer), [])\n\n  const focusRow = React.useCallback(\n    (index: number) => {\n      const row = rowsRef.current[index]\n      if (!row) return\n      pendingFocus.current = row.id\n      scrollToIndex?.(index)\n      setFocusedId(row.id)\n      onFocusedChange?.(row.id)\n    },\n    [scrollToIndex, onFocusedChange]\n  )\n\n  const setExpanded = React.useCallback(\n    (id: string, open: boolean) => {\n      const has = expandedList.includes(id)\n      if (open === has) return\n      setExpandedList(\n        open ? [...expandedList, id] : expandedList.filter((x) => x !== id)\n      )\n    },\n    [expandedList, setExpandedList]\n  )\n  const toggle = React.useCallback(\n    (id: string) => setExpanded(id, !expandedSet.has(id)),\n    [expandedSet, setExpanded]\n  )\n\n  const select = React.useCallback(\n    (row: TreeRow<T>, how: { toggle?: boolean; range?: boolean } = {}) => {\n      if (row.node.disabled) {\n        // A disabled branch cannot be chosen, but its children may be.\n        if (row.expandable) toggle(row.id)\n        return\n      }\n      if (isSelectable && !isSelectable(row.node)) {\n        if (row.expandable) toggle(row.id)\n        return\n      }\n      if (onItemToggle) {\n        onItemToggle(row.node, row)\n        return\n      }\n      if (selectionMode === \"none\") return\n      if (selectionMode === \"single\") {\n        setSelectedList([row.id])\n        anchorRef.current = row.id\n        return\n      }\n      const current = selectedList\n      if (how.range && anchorRef.current) {\n        const a = rowsRef.current.findIndex((r) => r.id === anchorRef.current)\n        const b = row.index\n        if (a >= 0) {\n          const [from, to] = a < b ? [a, b] : [b, a]\n          const span = rowsRef.current\n            .slice(from, to + 1)\n            .filter((r) => !r.node.disabled)\n            .map((r) => r.id)\n          setSelectedList([...new Set([...current, ...span])])\n          return\n        }\n      }\n      anchorRef.current = row.id\n      if (how.toggle || selectionBehavior === \"toggle\") {\n        setSelectedList(\n          current.includes(row.id)\n            ? current.filter((x) => x !== row.id)\n            : [...current, row.id]\n        )\n      } else setSelectedList([row.id])\n    },\n    [\n      onItemToggle,\n      isSelectable,\n      selectionMode,\n      selectionBehavior,\n      selectedList,\n      setSelectedList,\n      toggle,\n    ]\n  )\n\n  const onKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {\n    const target = event.target as HTMLElement\n    // Only the row itself (not a field or button inside it) drives the tree.\n    if (target.dataset.treeId === undefined) return\n    const list = rowsRef.current\n    const at = list.findIndex((r) => r.id === target.dataset.treeId)\n    if (at < 0) return\n    const row = list[at]\n    const rtl =\n      (dir ??\n        (target.closest(\"[dir]\")?.getAttribute(\"dir\") as\n          \"ltr\" | \"rtl\" | null) ??\n        (getComputedStyle(target).direction as \"ltr\" | \"rtl\")) === \"rtl\"\n    const forward = rtl ? \"ArrowLeft\" : \"ArrowRight\"\n    const back = rtl ? \"ArrowRight\" : \"ArrowLeft\"\n    const mod = event.ctrlKey || event.metaKey\n    const key = event.key\n\n    // Application keys first (alt+arrow moves in the sortable tree).\n    if (event.altKey && key.startsWith(\"Arrow\")) {\n      onRowKeyDown?.(event, row)\n      return\n    }\n\n    const go = (index: number) => {\n      event.preventDefault()\n      focusRow(Math.max(0, Math.min(list.length - 1, index)))\n    }\n\n    if (key === \"ArrowDown\" || key === \"ArrowUp\") {\n      const next = Math.max(\n        0,\n        Math.min(list.length - 1, at + (key === \"ArrowDown\" ? 1 : -1))\n      )\n      go(next)\n      if (event.shiftKey && selectionMode === \"multiple\" && !onItemToggle) {\n        select(list[next], { range: true })\n      }\n      return\n    }\n    if (key === forward) {\n      event.preventDefault()\n      if (!row.expandable) return\n      if (!row.expanded) setExpanded(row.id, true)\n      else if (list[at + 1]?.parentId === row.id) focusRow(at + 1)\n      return\n    }\n    if (key === back) {\n      event.preventDefault()\n      if (row.expanded) setExpanded(row.id, false)\n      else if (row.parentId) {\n        const parent = list.findIndex((r) => r.id === row.parentId)\n        if (parent >= 0) focusRow(parent)\n      }\n      return\n    }\n    if (key === \"Home\") return go(0)\n    if (key === \"End\") return go(list.length - 1)\n    if (key === \"PageDown\") return go(at + 10)\n    if (key === \"PageUp\") return go(at - 10)\n    if (key === \"*\") {\n      event.preventDefault()\n      const open = list\n        .filter((r) => r.parentId === row.parentId && r.expandable)\n        .map((r) => r.id)\n      setExpandedList([...new Set([...expandedList, ...open])])\n      return\n    }\n    if (key === \"Enter\") {\n      event.preventDefault()\n      if (row.node.disabled) return\n      if (onActivate && onActivate(row.node) !== false) return\n      if (row.node.href) target.click()\n      else if (row.expandable) toggle(row.id)\n      else select(row)\n      return\n    }\n    if (key === \" \") {\n      event.preventDefault()\n      select(row, { toggle: true })\n      return\n    }\n    if (\n      mod &&\n      key.toLowerCase() === \"a\" &&\n      selectionMode === \"multiple\" &&\n      !onItemToggle\n    ) {\n      event.preventDefault()\n      setSelectedList(list.filter((r) => !r.node.disabled).map((r) => r.id))\n      return\n    }\n    if (key.length === 1 && !mod && !event.altKey) {\n      const state = typeahead.current\n      window.clearTimeout(state.timer)\n      state.buffer += key\n      state.timer = window.setTimeout(() => (state.buffer = \"\"), 600)\n      const found = findTypeaheadMatch(list, at, state.buffer)\n      if (found >= 0) go(found)\n      return\n    }\n    onRowKeyDown?.(event, row)\n  }\n\n  const rootProps = {\n    ref: rootRef,\n    role: \"tree\" as const,\n    \"aria-multiselectable\": selectionMode === \"multiple\" ? true : undefined,\n    onKeyDown,\n  }\n\n  const getRowProps = (row: TreeRow<T>) => ({\n    role: \"treeitem\" as const,\n    \"aria-level\": row.level,\n    \"aria-posinset\": row.posInSet,\n    \"aria-setsize\": row.setSize,\n    \"aria-expanded\": row.expandable ? row.expanded : undefined,\n    \"aria-selected\":\n      selectionMode === \"none\" || onItemToggle\n        ? undefined\n        : selectedSet.has(row.id),\n    \"aria-disabled\": row.node.disabled || undefined,\n    tabIndex: row.id === tabbableId ? 0 : -1,\n    \"data-tree-id\": row.id,\n    \"data-state\": row.expandable\n      ? row.expanded\n        ? \"open\"\n        : \"closed\"\n      : undefined,\n    \"data-selected\": selectedSet.has(row.id) ? \"\" : undefined,\n    \"data-disabled\": row.node.disabled ? \"\" : undefined,\n    onFocus: (event: React.FocusEvent<HTMLElement>) => {\n      if (event.target !== event.currentTarget) return\n      setFocusedId(row.id)\n      onFocusedChange?.(row.id)\n    },\n    onClick: (event: React.MouseEvent<HTMLElement>) => {\n      if ((event.target as HTMLElement).closest(\"[data-tree-toggle]\")) return\n      select(row, {\n        toggle: event.ctrlKey || event.metaKey,\n        range: event.shiftKey,\n      })\n    },\n    onDoubleClick: (event: React.MouseEvent<HTMLElement>) => {\n      if ((event.target as HTMLElement).closest(\"[data-tree-toggle]\")) return\n      if (row.node.disabled) return\n      if (onActivate && onActivate(row.node) !== false) return\n      if (row.expandable) toggle(row.id)\n    },\n  })\n\n  return {\n    rows,\n    expandedSet,\n    selectedSet,\n    focusedId: tabbableId,\n    rootProps,\n    getRowProps,\n    toggle,\n    setExpanded,\n    select,\n    focusRow,\n    setSelected: setSelectedList,\n    setExpandedIds: setExpandedList,\n    expandedList,\n    selectedList,\n  }\n}\n\nexport type TreeController<T = unknown> = ReturnType<typeof useTree<T>>\n\n/* -------------------------------------------------------------------------- */\n/*  View                                                                      */\n/* -------------------------------------------------------------------------- */\n\nexport const treeRowClasses =\n  \"group/tree-row relative flex min-h-8 cursor-default items-center gap-1.5 rounded-md pe-2 text-sm outline-none select-none \" +\n  \"text-foreground hover:bg-accent/60 focus-visible:ring-2 focus-visible:ring-ring/60 \" +\n  \"data-[selected]:bg-accent data-[selected]:text-accent-foreground \" +\n  \"data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50 data-[disabled]:hover:bg-transparent\"\n\nexport interface TreeRowContentProps<T = unknown> {\n  row: TreeRow<T>\n  controller: Pick<TreeController<T>, \"toggle\">\n  indent?: number\n  guides?: boolean\n  /** Replaces the label text. */\n  label?: React.ReactNode\n  /** Shown instead of the chevron while children load. */\n  loading?: boolean\n  /** Content before the label, after the chevron (checkbox, icon). */\n  leading?: React.ReactNode\n  /** Content at the end of the row. */\n  trailing?: React.ReactNode\n}\n\n/** Indent, guide lines, chevron, icon and label of one row. */\nexport function TreeRowContent<T>({\n  row,\n  controller,\n  indent = 16,\n  guides = false,\n  label,\n  loading = false,\n  leading,\n  trailing,\n}: TreeRowContentProps<T>) {\n  return (\n    <>\n      {guides &&\n        Array.from({ length: row.level - 1 }, (_, i) => (\n          <span\n            key={i}\n            aria-hidden\n            className=\"bg-border pointer-events-none absolute inset-y-0 w-px\"\n            style={{ insetInlineStart: i * indent + 16 }}\n          />\n        ))}\n      <span\n        aria-hidden\n        className=\"shrink-0\"\n        style={{ width: (row.level - 1) * indent }}\n      />\n      <span\n        aria-hidden\n        data-tree-toggle=\"\"\n        className={cn(\n          \"text-muted-foreground flex size-5 shrink-0 items-center justify-center rounded-sm\",\n          row.expandable && \"hover:bg-foreground/10 cursor-pointer\"\n        )}\n        onClick={(event) => {\n          if (!row.expandable) return\n          // Inside a link row the click must not follow the link.\n          event.preventDefault()\n          event.stopPropagation()\n          controller.toggle(row.id)\n        }}\n      >\n        {loading ? (\n          <Loader2 className=\"size-3.5 animate-spin motion-reduce:animate-none\" />\n        ) : row.expandable ? (\n          row.expanded ? (\n            <ChevronDown className=\"size-3.5\" />\n          ) : (\n            <ChevronRight className=\"size-3.5 rtl:rotate-180\" />\n          )\n        ) : null}\n      </span>\n      {leading}\n      {row.node.icon ? (\n        <span\n          aria-hidden\n          className=\"text-muted-foreground shrink-0 [&_svg]:size-4\"\n        >\n          {row.node.icon}\n        </span>\n      ) : null}\n      <span className=\"min-w-0 flex-1 truncate\">{label ?? row.node.label}</span>\n      {trailing}\n    </>\n  )\n}\n\nexport interface TreeViewProps<T = unknown>\n  extends\n    Omit<UseTreeOptions<T>, \"nodes\">,\n    Omit<React.ComponentProps<\"div\">, \"onSelect\" | \"defaultValue\" | \"dir\"> {\n  nodes: TreeNode<T>[]\n  /** Pixels of indent per level. Default 16. */\n  indent?: number\n  /** Vertical guide lines for each level. */\n  guides?: boolean\n  /** Replaces the label of a row (highlighting, badges). */\n  renderLabel?: (row: TreeRow<T>) => React.ReactNode\n  /** Content at the end of a row. */\n  renderTrailing?: (row: TreeRow<T>) => React.ReactNode\n  /** Class names for every row. */\n  rowClassName?: string\n  /** Extra attributes for a row (aria-current, data-*). */\n  rowProps?: (row: TreeRow<T>) => Record<string, unknown>\n  /** Shows a spinner in place of the chevron and sets aria-busy. */\n  isRowLoading?: (row: TreeRow<T>) => boolean\n}\n\n/**\n * An ARIA tree: roving focus, arrow keys (Right/Left swap in rtl), Home, End,\n * `*`, typeahead, Enter and Space, single or multiple selection.\n */\nexport function TreeView<T = unknown>({\n  nodes,\n  indent = 16,\n  guides,\n  renderLabel,\n  renderTrailing,\n  rowClassName,\n  rowProps: extraRowProps,\n  isRowLoading,\n  className,\n  expanded,\n  defaultExpanded,\n  onExpandedChange,\n  selected,\n  defaultSelected,\n  onSelectedChange,\n  selectionMode,\n  selectionBehavior,\n  onActivate,\n  onItemToggle,\n  isSelectable,\n  scrollToIndex,\n  defaultFocusedId,\n  onFocusedChange,\n  onRowKeyDown,\n  dir,\n  ...props\n}: TreeViewProps<T>) {\n  const tree = useTree<T>({\n    nodes,\n    expanded,\n    defaultExpanded,\n    onExpandedChange,\n    selected,\n    defaultSelected,\n    onSelectedChange,\n    selectionMode,\n    selectionBehavior,\n    onActivate,\n    onItemToggle,\n    isSelectable,\n    scrollToIndex,\n    defaultFocusedId,\n    onFocusedChange,\n    onRowKeyDown,\n    dir,\n  })\n  const { ref, ...root } = tree.rootProps\n  return (\n    <div\n      ref={ref}\n      {...root}\n      {...props}\n      className={cn(\"flex flex-col gap-px\", className)}\n    >\n      {tree.rows.map((row) => {\n        const loading = isRowLoading?.(row) ?? false\n        const rowProps = {\n          ...tree.getRowProps(row),\n          \"aria-busy\": loading || undefined,\n          ...extraRowProps?.(row),\n        }\n        const content = (\n          <TreeRowContent\n            row={row}\n            controller={tree}\n            indent={indent}\n            guides={guides}\n            label={renderLabel?.(row)}\n            loading={loading}\n            trailing={renderTrailing?.(row)}\n          />\n        )\n        return row.node.href ? (\n          <a\n            key={row.id}\n            href={row.node.href}\n            {...rowProps}\n            className={cn(treeRowClasses, rowClassName)}\n          >\n            {content}\n          </a>\n        ) : (\n          <div\n            key={row.id}\n            {...rowProps}\n            className={cn(treeRowClasses, rowClassName)}\n          >\n            {content}\n          </div>\n        )\n      })}\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}