{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tree-view",
  "type": "registry:ui",
  "title": "Tree View",
  "description": "A file-explorer tree on the APG pattern: arrow keys, typeahead, multi-select, tri-state checkboxes and lazy loading.",
  "author": "WANSTUDIO <https://wanstudio.my>",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/wancraft/tree-view.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion } from \"motion/react\";\n\nexport interface TreeNode {\n  /** Unique id across the whole tree. */\n  id: string;\n  /** Visible label; also used for typeahead. */\n  label: string;\n  /** Icon shown before the label. Defaults to a folder or file glyph. */\n  icon?: React.ReactNode;\n  /** Child nodes. Omit and set `hasChildren` to load them lazily with `loadChildren`. */\n  children?: TreeNode[];\n  /** Marks a parent whose children are loaded on first expand. */\n  hasChildren?: boolean;\n  /** Focusable but not selectable or checkable. */\n  disabled?: boolean;\n}\n\nexport interface TreeViewProps\n  extends Omit<React.HTMLAttributes<HTMLUListElement>, \"children\" | \"onSelect\" | \"defaultValue\" | \"defaultChecked\"> {\n  items: TreeNode[];\n  /** Accessible name of the tree. */\n  label?: string;\n  /** Expanded node ids (controlled). */\n  expanded?: string[];\n  /** Initially expanded node ids (uncontrolled). */\n  defaultExpanded?: string[];\n  onExpandedChange?: (ids: string[]) => void;\n  /** Selected node ids (controlled). */\n  selected?: string[];\n  /** Initially selected node ids (uncontrolled). */\n  defaultSelected?: string[];\n  onSelectedChange?: (ids: string[]) => void;\n  /** \"single\": selection follows focus. \"multiple\": Ctrl / Shift click and Shift + arrows. */\n  selectionMode?: \"none\" | \"single\" | \"multiple\";\n  /** Tri-state checkboxes (aria-checked). Replaces selection. */\n  checkboxes?: boolean;\n  /** Checked ids (controlled). Store leaf ids; parent states are derived. */\n  checked?: string[];\n  /** Initially checked ids (uncontrolled). */\n  defaultChecked?: string[];\n  onCheckedChange?: (ids: string[]) => void;\n  /** Loads children for nodes with `hasChildren` the first time they expand. */\n  loadChildren?: (node: TreeNode) => Promise<TreeNode[]>;\n  /** Enter or double-click on a node. */\n  onAction?: (node: TreeNode) => void;\n  /** Indentation per level in px. */\n  indent?: number;\n  /** Vertical guide lines for each level. */\n  showGuides?: boolean;\n  /** Row density. */\n  size?: \"sm\" | \"md\";\n  /** Expand / collapse duration in ms. */\n  duration?: number;\n  /** Selection, checkbox and focus colour. */\n  accent?: string;\n}\n\ntype CheckState = boolean | \"mixed\";\n\ninterface Row {\n  node: TreeNode;\n  level: number;\n  parentId: string | null;\n}\n\nfunction subscribeReducedMotion(callback: () => void) {\n  const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n  mq.addEventListener(\"change\", callback);\n  return () => mq.removeEventListener(\"change\", callback);\n}\n\n/** SSR-safe: hydrates as \"no preference\", then follows the media query. */\nfunction usePrefersReducedMotion() {\n  return React.useSyncExternalStore(\n    subscribeReducedMotion,\n    () => window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches,\n    () => false,\n  );\n}\n\n/** Controlled / uncontrolled string-array state. */\nfunction useIdList(value: string[] | undefined, defaultValue: string[] | undefined, onChange?: (ids: string[]) => void) {\n  const [internal, setInternal] = React.useState<string[]>(defaultValue ?? []);\n  const current = value ?? internal;\n  const set = (next: string[]) => {\n    if (value === undefined) setInternal(next);\n    onChange?.(next);\n  };\n  return [current, set] as const;\n}\n\n/**\n * A file-explorer tree implementing the WAI-ARIA tree pattern: roving focus, arrow / Home / End navigation,\n * typeahead, \"*\" to expand siblings, single or multiple selection, optional tri-state checkboxes, lazily\n * loaded children with a spinner, and animated expand / collapse.\n */\nexport function TreeView({\n  items,\n  label = \"Tree\",\n  expanded: expandedProp,\n  defaultExpanded,\n  onExpandedChange,\n  selected: selectedProp,\n  defaultSelected,\n  onSelectedChange,\n  selectionMode = \"single\",\n  checkboxes = false,\n  checked: checkedProp,\n  defaultChecked,\n  onCheckedChange,\n  loadChildren,\n  onAction,\n  indent = 16,\n  showGuides = true,\n  size = \"md\",\n  duration = 200,\n  accent = \"#FF7448\",\n  className = \"\",\n  style,\n  onKeyDown,\n  ...props\n}: TreeViewProps) {\n  const uid = React.useId();\n  const reduceMotion = usePrefersReducedMotion();\n  const rootRef = React.useRef<HTMLUListElement>(null);\n  const itemRefs = React.useRef(new Map<string, HTMLLIElement>());\n  const typeaheadRef = React.useRef({ buffer: \"\", timer: 0 });\n  const anchorRef = React.useRef<string | null>(null);\n\n  const [expandedIds, setExpandedIds] = useIdList(expandedProp, defaultExpanded, onExpandedChange);\n  const [selectedIds, setSelectedIds] = useIdList(selectedProp, defaultSelected, onSelectedChange);\n  const [checkedIds, setCheckedIds] = useIdList(checkedProp, defaultChecked, onCheckedChange);\n  const [loaded, setLoaded] = React.useState<Record<string, TreeNode[]>>({});\n  const [loading, setLoading] = React.useState<string[]>([]);\n  const [focusedId, setFocusedId] = React.useState<string | null>(null);\n  const [announcement, setAnnouncement] = React.useState(\"\");\n\n  // Latest lists for async completions (a load can resolve after several re-renders).\n  const latestRef = React.useRef({ expandedIds, checkedIds });\n  React.useEffect(() => {\n    latestRef.current = { expandedIds, checkedIds };\n  });\n\n  const expandedSet = new Set(expandedIds);\n  const selectedSet = new Set(selectedIds);\n  const checkedSet = new Set(checkedIds);\n  const selectable = !checkboxes && selectionMode !== \"none\";\n  const multiple = selectable && selectionMode === \"multiple\";\n\n  const childrenOf = (node: TreeNode): TreeNode[] | undefined => node.children ?? loaded[node.id];\n  const isParent = (node: TreeNode) => Boolean(childrenOf(node)?.length) || Boolean(node.hasChildren && !childrenOf(node));\n\n  // Every known node by id (loaded or not visible), plus the visible rows in order.\n  const byId = new Map<string, Row>();\n  const indexAll = (nodes: TreeNode[], level: number, parentId: string | null) => {\n    for (const node of nodes) {\n      byId.set(node.id, { node, level, parentId });\n      const kids = childrenOf(node);\n      if (kids) indexAll(kids, level + 1, node.id);\n    }\n  };\n  indexAll(items, 1, null);\n\n  const visible: Row[] = [];\n  const flatten = (nodes: TreeNode[]) => {\n    for (const node of nodes) {\n      visible.push(byId.get(node.id)!);\n      const kids = childrenOf(node);\n      if (kids && expandedSet.has(node.id)) flatten(kids);\n    }\n  };\n  flatten(items);\n  const visibleIndex = (id: string | null) => visible.findIndex((row) => row.node.id === id);\n\n  // Tab stop: the focused row if visible, else the first selected visible row, else the first row.\n  const tabStop =\n    (focusedId && visibleIndex(focusedId) >= 0 ? focusedId : null) ??\n    visible.find((row) => selectedSet.has(row.node.id))?.node.id ??\n    visible[0]?.node.id ??\n    null;\n\n  /* --------------------------------- checks --------------------------------- */\n\n  const checkCache = new Map<string, CheckState>();\n  const checkState = (node: TreeNode): CheckState => {\n    const cached = checkCache.get(node.id);\n    if (cached !== undefined) return cached;\n    const kids = childrenOf(node);\n    let state: CheckState;\n    if (!kids?.length) state = checkedSet.has(node.id);\n    else {\n      let all = true;\n      let none = true;\n      for (const kid of kids) {\n        const s = checkState(kid);\n        if (s !== true) all = false;\n        if (s !== false) none = false;\n      }\n      state = all ? true : none ? false : \"mixed\";\n    }\n    checkCache.set(node.id, state);\n    return state;\n  };\n\n  /** Leaf ids under a node (a node without known children counts as its own leaf). */\n  const leavesOf = (node: TreeNode): string[] => {\n    const kids = childrenOf(node);\n    if (!kids?.length) return node.disabled ? [] : [node.id];\n    return kids.flatMap(leavesOf);\n  };\n\n  const toggleChecked = (node: TreeNode) => {\n    if (node.disabled) return;\n    const next = new Set(checkedIds);\n    const turnOn = checkState(node) !== true;\n    for (const id of leavesOf(node)) {\n      if (turnOn) next.add(id);\n      else next.delete(id);\n    }\n    setCheckedIds([...next]);\n  };\n\n  /* ------------------------------ expand / load ------------------------------ */\n\n  const load = (node: TreeNode) => {\n    if (!loadChildren || loading.includes(node.id)) return;\n    setLoading((prev) => [...prev, node.id]);\n    setAnnouncement(`Loading ${node.label}…`);\n    loadChildren(node).then(\n      (kids) => {\n        setLoaded((prev) => ({ ...prev, [node.id]: kids }));\n        setLoading((prev) => prev.filter((id) => id !== node.id));\n        setAnnouncement(`${node.label}: ${kids.length} ${kids.length === 1 ? \"item\" : \"items\"} loaded`);\n        // A checked placeholder hands its state down to the newly loaded children.\n        const latestChecked = latestRef.current.checkedIds;\n        if (latestChecked.includes(node.id)) {\n          const next = new Set(latestChecked.filter((id) => id !== node.id));\n          kids.forEach((kid) => !kid.disabled && next.add(kid.id));\n          setCheckedIds([...next]);\n        }\n      },\n      () => {\n        setLoading((prev) => prev.filter((id) => id !== node.id));\n        setExpandedIds(latestRef.current.expandedIds.filter((id) => id !== node.id));\n        setAnnouncement(`Could not load ${node.label}`);\n      },\n    );\n  };\n\n  const setExpanded = (node: TreeNode, open: boolean) => {\n    if (!isParent(node) || expandedSet.has(node.id) === open) return;\n    setExpandedIds(open ? [...expandedIds, node.id] : expandedIds.filter((id) => id !== node.id));\n    if (open && !childrenOf(node) && node.hasChildren) load(node);\n  };\n\n  /* -------------------------------- selection -------------------------------- */\n\n  const rangeIds = (fromId: string, toId: string) => {\n    const a = visibleIndex(fromId);\n    const b = visibleIndex(toId);\n    if (a < 0 || b < 0) return [toId];\n    const [start, end] = a < b ? [a, b] : [b, a];\n    return visible.slice(start, end + 1).filter((row) => !row.node.disabled).map((row) => row.node.id);\n  };\n\n  const selectOnly = (node: TreeNode) => {\n    if (!selectable || node.disabled) return;\n    anchorRef.current = node.id;\n    if (selectedIds.length !== 1 || selectedIds[0] !== node.id) setSelectedIds([node.id]);\n  };\n\n  const toggleSelected = (node: TreeNode) => {\n    if (!multiple || node.disabled) return;\n    anchorRef.current = node.id;\n    setSelectedIds(selectedSet.has(node.id) ? selectedIds.filter((id) => id !== node.id) : [...selectedIds, node.id]);\n  };\n\n  const selectRange = (toId: string, additive = false) => {\n    if (!multiple) return;\n    const anchor = anchorRef.current ?? tabStop ?? toId;\n    const range = rangeIds(anchor, toId);\n    setSelectedIds(additive ? [...new Set([...selectedIds, ...range])] : range);\n  };\n\n  /* ---------------------------------- focus ---------------------------------- */\n\n  /** Keeps the row inside the tree's own scroll area without scrolling the page. */\n  const reveal = (id: string) => {\n    const root = rootRef.current;\n    const row = itemRefs.current.get(id)?.querySelector<HTMLElement>(\"[data-row]\");\n    if (!root || !row || root.scrollHeight <= root.clientHeight) return;\n    const rootRect = root.getBoundingClientRect();\n    const rect = row.getBoundingClientRect();\n    if (rect.top < rootRect.top) root.scrollTop += rect.top - rootRect.top - 4;\n    else if (rect.bottom > rootRect.bottom) root.scrollTop += rect.bottom - rootRect.bottom + 4;\n  };\n\n  const focusRow = (id: string) => {\n    setFocusedId(id);\n    itemRefs.current.get(id)?.focus({ preventScroll: true });\n    reveal(id);\n  };\n\n  /* -------------------------------- keyboard -------------------------------- */\n\n  const typeahead = (char: string, fromIndex: number) => {\n    const state = typeaheadRef.current;\n    window.clearTimeout(state.timer);\n    state.buffer += char.toLowerCase();\n    state.timer = window.setTimeout(() => (state.buffer = \"\"), 500);\n    const start = state.buffer.length === 1 ? fromIndex + 1 : fromIndex;\n    for (let i = 0; i < visible.length; i++) {\n      const row = visible[(start + i) % visible.length];\n      if (row.node.label.toLowerCase().startsWith(state.buffer)) return row.node.id;\n    }\n    return null;\n  };\n\n  const handleKeyDown = (event: React.KeyboardEvent<HTMLUListElement>) => {\n    onKeyDown?.(event);\n    if (event.defaultPrevented) return;\n    const li = (event.target as HTMLElement).closest<HTMLLIElement>(\"[role=treeitem]\");\n    const id = li?.dataset.id;\n    if (!id || li !== event.target) return;\n    const row = byId.get(id);\n    const index = visibleIndex(id);\n    if (!row || index < 0) return;\n    const { node } = row;\n    const mod = event.ctrlKey || event.metaKey;\n\n    const move = (targetIndex: number) => {\n      const target = visible[Math.max(0, Math.min(visible.length - 1, targetIndex))];\n      if (!target) return;\n      focusRow(target.node.id);\n      if (multiple && event.shiftKey) {\n        if (mod) selectRange(target.node.id, true);\n        else toggleSelected(target.node);\n      } else if (selectionMode === \"single\" && selectable) selectOnly(target.node);\n    };\n\n    let handled = true;\n    switch (event.key) {\n      case \"ArrowDown\":\n        move(index + 1);\n        break;\n      case \"ArrowUp\":\n        move(index - 1);\n        break;\n      case \"ArrowRight\":\n        if (isParent(node)) {\n          if (!expandedSet.has(node.id)) setExpanded(node, true);\n          else if (childrenOf(node)?.length) move(index + 1);\n        }\n        break;\n      case \"ArrowLeft\":\n        if (isParent(node) && expandedSet.has(node.id)) setExpanded(node, false);\n        else if (row.parentId) move(visibleIndex(row.parentId));\n        break;\n      case \"Home\":\n        if (multiple && event.shiftKey && mod) selectRange(visible[0].node.id, true);\n        move(0);\n        break;\n      case \"End\":\n        if (multiple && event.shiftKey && mod) selectRange(visible[visible.length - 1].node.id, true);\n        move(visible.length - 1);\n        break;\n      case \"Enter\":\n        if (isParent(node)) setExpanded(node, !expandedSet.has(node.id));\n        if (!multiple) selectOnly(node);\n        onAction?.(node);\n        break;\n      case \" \":\n        if (checkboxes) toggleChecked(node);\n        else if (multiple && event.shiftKey) selectRange(node.id);\n        else if (multiple) toggleSelected(node);\n        else selectOnly(node);\n        break;\n      case \"*\": {\n        const siblings = row.parentId ? (childrenOf(byId.get(row.parentId)!.node) ?? []) : items;\n        const toOpen = siblings.filter((sibling) => isParent(sibling) && !expandedSet.has(sibling.id));\n        if (toOpen.length) {\n          setExpandedIds([...expandedIds, ...toOpen.map((sibling) => sibling.id)]);\n          toOpen.forEach((sibling) => !childrenOf(sibling) && sibling.hasChildren && load(sibling));\n        }\n        break;\n      }\n      default:\n        if (multiple && mod && event.key.toLowerCase() === \"a\") {\n          setSelectedIds(visible.filter((r) => !r.node.disabled).map((r) => r.node.id));\n        } else if (event.key.length === 1 && !mod && !event.altKey && event.key.trim()) {\n          const match = typeahead(event.key, index);\n          if (match) focusRow(match);\n          if (match && selectionMode === \"single\" && selectable) selectOnly(byId.get(match)!.node);\n        } else handled = false;\n    }\n    if (handled) event.preventDefault();\n  };\n\n  /* --------------------------------- pointer --------------------------------- */\n\n  const handleRowClick = (event: React.MouseEvent, node: TreeNode) => {\n    focusRow(node.id);\n    if (checkboxes) {\n      if (isParent(node)) setExpanded(node, !expandedSet.has(node.id));\n      else toggleChecked(node);\n      return;\n    }\n    if (multiple && event.shiftKey) {\n      selectRange(node.id, event.ctrlKey || event.metaKey);\n      return;\n    }\n    if (multiple && (event.ctrlKey || event.metaKey)) {\n      toggleSelected(node);\n      return;\n    }\n    selectOnly(node);\n    if (isParent(node)) setExpanded(node, !expandedSet.has(node.id));\n  };\n\n  /* ---------------------------------- render --------------------------------- */\n\n  const rowHeight = size === \"sm\" ? \"h-7 text-[12.5px]\" : \"h-8 text-[13px]\";\n  const transition = { duration: reduceMotion ? 0 : duration / 1000, ease: [0.22, 1, 0.36, 1] as const };\n  const GUTTER = 8; // px before the first chevron\n\n  const renderNodes = (nodes: TreeNode[], level: number): React.ReactNode =>\n    nodes.map((node, i) => {\n      const kids = childrenOf(node);\n      const parent = isParent(node);\n      const isExpanded = parent && expandedSet.has(node.id);\n      const isLoading = loading.includes(node.id);\n      const isSelected = selectable && selectedSet.has(node.id);\n      const check = checkboxes ? checkState(node) : false;\n      const labelId = `${uid}-${node.id}-label`;\n      const offset = GUTTER + (level - 1) * indent;\n\n      return (\n        <li\n          key={node.id}\n          ref={(el) => {\n            if (el) itemRefs.current.set(node.id, el);\n            else itemRefs.current.delete(node.id);\n          }}\n          role=\"treeitem\"\n          data-id={node.id}\n          aria-labelledby={labelId}\n          aria-level={level}\n          aria-setsize={nodes.length}\n          aria-posinset={i + 1}\n          aria-expanded={parent ? isExpanded : undefined}\n          aria-selected={selectable ? isSelected : undefined}\n          aria-checked={checkboxes ? check : undefined}\n          aria-busy={isLoading || undefined}\n          aria-disabled={node.disabled || undefined}\n          tabIndex={node.id === tabStop ? 0 : -1}\n          onFocus={(event) => {\n            if (event.target === event.currentTarget) setFocusedId(node.id);\n          }}\n          className=\"outline-hidden [&:focus-visible>[data-row]]:outline-2 [&:focus-visible>[data-row]]:-outline-offset-2 [&:focus-visible>[data-row]]:outline-solid [&:focus-visible>[data-row]]:outline-[var(--tv-accent)]\"\n        >\n          <div\n            data-row=\"\"\n            onClick={(event) => handleRowClick(event, node)}\n            onDoubleClick={() => onAction?.(node)}\n            className={`relative flex cursor-default select-none items-center gap-1.5 rounded-md pr-2 transition-colors duration-150 ${rowHeight} ${\n              isSelected\n                ? \"bg-[color-mix(in_oklab,var(--tv-accent)_14%,transparent)] text-zinc-950 dark:bg-[color-mix(in_oklab,var(--tv-accent)_20%,transparent)] dark:text-white\"\n                : \"text-zinc-700 hover:bg-zinc-900/[0.045] dark:text-zinc-300 dark:hover:bg-white/[0.05]\"\n            } ${node.disabled ? \"opacity-45\" : \"\"}`}\n            style={{ paddingLeft: offset }}\n          >\n            {showGuides &&\n              Array.from({ length: level - 1 }, (_, l) => (\n                <span\n                  key={l}\n                  aria-hidden=\"true\"\n                  className=\"absolute inset-y-0 w-px bg-zinc-900/10 dark:bg-white/10\"\n                  style={{ left: GUTTER + l * indent + 7.5 }}\n                />\n              ))}\n            {parent ? (\n              <span\n                aria-hidden=\"true\"\n                onClick={(event) => {\n                  // The chevron only toggles, it never changes the selection.\n                  event.stopPropagation();\n                  focusRow(node.id);\n                  setExpanded(node, !isExpanded);\n                }}\n                className=\"flex size-4 shrink-0 items-center justify-center rounded text-zinc-400 hover:text-zinc-700 dark:text-zinc-500 dark:hover:text-zinc-200\"\n              >\n                <svg\n                  viewBox=\"0 0 16 16\"\n                  className={`size-3 ${isExpanded ? \"rotate-90\" : \"\"} ${reduceMotion ? \"\" : \"transition-transform duration-200\"}`}\n                  fill=\"none\"\n                >\n                  <path d=\"M6 3.5 10.5 8 6 12.5\" stroke=\"currentColor\" strokeWidth={1.75} strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n                </svg>\n              </span>\n            ) : (\n              <span aria-hidden=\"true\" className=\"size-4 shrink-0\" />\n            )}\n            {checkboxes && (\n              <span\n                aria-hidden=\"true\"\n                onClick={(event) => {\n                  event.stopPropagation();\n                  focusRow(node.id);\n                  toggleChecked(node);\n                }}\n                className={`flex size-[15px] shrink-0 items-center justify-center rounded-[4px] border transition-colors ${\n                  check\n                    ? \"border-[var(--tv-accent)] bg-[var(--tv-accent)] text-white\"\n                    : \"border-zinc-900/25 bg-white dark:border-white/25 dark:bg-zinc-900\"\n                }`}\n              >\n                {check === true && (\n                  <svg viewBox=\"0 0 12 12\" className=\"size-2.5\" fill=\"none\">\n                    <path d=\"m2.5 6.2 2.2 2.2 4.8-4.9\" stroke=\"currentColor\" strokeWidth={1.8} strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n                  </svg>\n                )}\n                {check === \"mixed\" && <span className=\"h-[1.75px] w-2 rounded-full bg-current\" />}\n              </span>\n            )}\n            <span\n              aria-hidden=\"true\"\n              className={`flex size-4 shrink-0 items-center justify-center [&_svg]:size-4 ${\n                isSelected ? \"text-[var(--tv-accent)]\" : \"text-zinc-400 dark:text-zinc-500\"\n              }`}\n            >\n              {node.icon ?? (parent ? <FolderGlyph open={isExpanded} /> : <FileGlyph />)}\n            </span>\n            <span id={labelId} className=\"min-w-0 flex-1 truncate tracking-tight\">\n              {node.label}\n            </span>\n            {isLoading && (\n              <svg aria-hidden=\"true\" viewBox=\"0 0 16 16\" className={`size-3.5 shrink-0 text-[var(--tv-accent)] ${reduceMotion ? \"\" : \"animate-spin\"}`} fill=\"none\">\n                <circle cx=\"8\" cy=\"8\" r=\"6\" stroke=\"currentColor\" strokeOpacity=\".25\" strokeWidth=\"2\" />\n                <path d=\"M14 8a6 6 0 0 0-6-6\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" />\n              </svg>\n            )}\n          </div>\n          <AnimatePresence initial={false}>\n            {isExpanded && kids && kids.length > 0 && (\n              <motion.ul\n                key=\"group\"\n                role=\"group\"\n                initial={{ height: 0, opacity: 0 }}\n                animate={{ height: \"auto\", opacity: 1 }}\n                exit={{ height: 0, opacity: 0 }}\n                transition={transition}\n                className=\"overflow-hidden\"\n              >\n                {renderNodes(kids, level + 1)}\n              </motion.ul>\n            )}\n          </AnimatePresence>\n        </li>\n      );\n    });\n\n  return (\n    <>\n      <ul\n        ref={rootRef}\n        role=\"tree\"\n        aria-label={label}\n        aria-multiselectable={multiple || undefined}\n        onKeyDown={handleKeyDown}\n        className={`relative overflow-auto overscroll-contain ${className}`}\n        style={{ [\"--tv-accent\" as string]: accent, ...style }}\n        {...props}\n      >\n        {renderNodes(items, 1)}\n      </ul>\n      <div role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n        {announcement}\n      </div>\n    </>\n  );\n}\n\nfunction FolderGlyph({ open }: { open: boolean }) {\n  return open ? (\n    <svg viewBox=\"0 0 16 16\" fill=\"none\">\n      <path d=\"M1.75 4.25c0-.69.56-1.25 1.25-1.25h3l1.5 1.5H13c.69 0 1.25.56 1.25 1.25V6\" stroke=\"currentColor\" strokeWidth=\"1.25\" strokeLinejoin=\"round\" />\n      <path d=\"M1.9 12.4 3.4 7.1c.15-.5.6-.85 1.13-.85h9.6c.75 0 1.28.72 1.07 1.43l-1.33 4.5c-.15.5-.6.82-1.12.82H3a1.1 1.1 0 0 1-1.1-.6Z\" stroke=\"currentColor\" strokeWidth=\"1.25\" strokeLinejoin=\"round\" />\n    </svg>\n  ) : (\n    <svg viewBox=\"0 0 16 16\" fill=\"none\">\n      <path d=\"M1.75 4.25c0-.69.56-1.25 1.25-1.25h3l1.5 1.5H13c.69 0 1.25.56 1.25 1.25v6c0 .69-.56 1.25-1.25 1.25H3c-.69 0-1.25-.56-1.25-1.25v-7.5Z\" stroke=\"currentColor\" strokeWidth=\"1.25\" strokeLinejoin=\"round\" />\n    </svg>\n  );\n}\n\nfunction FileGlyph() {\n  return (\n    <svg viewBox=\"0 0 16 16\" fill=\"none\">\n      <path d=\"M4 1.75h5l3.25 3.25v8c0 .69-.56 1.25-1.25 1.25H4c-.69 0-1.25-.56-1.25-1.25V3c0-.69.56-1.25 1.25-1.25Z\" stroke=\"currentColor\" strokeWidth=\"1.25\" strokeLinejoin=\"round\" />\n      <path d=\"M9 1.75V5h3.25\" stroke=\"currentColor\" strokeWidth=\"1.25\" strokeLinejoin=\"round\" />\n    </svg>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "navigation"
  ],
  "docs": "Documentation: https://wancraft.my/components/tree-view"
}
