{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tree-view",
  "type": "registry:ui",
  "title": "Tree View",
  "description": "A file tree that opens by height, lights its branch and types ahead.",
  "categories": [
    "navigation"
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json",
    "https://www.stealth.pm/r/icons.json",
    "https://www.stealth.pm/r/motion.json",
    "https://www.stealth.pm/r/use-controllable-state.json"
  ],
  "files": [
    {
      "path": "components/ui/tree-view.tsx",
      "content": "\"use client\";\nimport { AnimatePresence, animate, motion, useMotionValue, useReducedMotion } from \"motion/react\";\nimport { useCallback, useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { ChevronRight } from \"@/lib/icons\";\nimport { ease, spring } from \"@/lib/motion\";\nimport { useControllableState } from \"@/lib/use-controllable-state\";\n\nexport type TreeNode = {\n  id: string;\n  label: string;\n  /** Replaces the default folder or file glyph. */\n  icon?: React.ReactNode;\n  /** Child nodes. An empty array is an empty folder. */\n  children?: TreeNode[];\n  /** A folder whose children come from loadChildren the first time it opens. */\n  hasChildren?: boolean;\n  disabled?: boolean;\n  /** Quiet right-aligned detail: a size, a count, a status. */\n  meta?: React.ReactNode;\n};\n\nexport type TreeViewProps = Omit<React.ComponentProps<\"div\">, \"children\" | \"onSelect\"> & {\n  items: TreeNode[];\n  expanded?: string[];\n  defaultExpanded?: string[];\n  onExpandedChange?: (ids: string[]) => void;\n  selected?: string[];\n  defaultSelected?: string[];\n  onSelectedChange?: (ids: string[]) => void;\n  selectionMode?: \"single\" | \"multiple\" | \"none\";\n  /** Arrow keys move the selection along with focus, the way file explorers do. Single mode only. */\n  selectionFollowsFocus?: boolean;\n  /** Clicking a folder's row also opens or closes it. The chevron always does. */\n  expandOnClick?: boolean;\n  /** Enter or double-click on a leaf. */\n  onAction?: (node: TreeNode) => void;\n  /** Fetches the children of a node marked hasChildren, the first time it opens. */\n  loadChildren?: (node: TreeNode) => Promise<TreeNode[]>;\n  /** Vertical lines under each open folder. The branch holding the selection draws brighter. */\n  indentGuides?: boolean;\n};\n\ntype Row =\n  | { kind: \"node\"; node: TreeNode; level: number; parent: string | null; ancestors: string[]; pos: number; size: number; branch: boolean; open: boolean }\n  | { kind: \"status\"; status: \"loading\" | \"error\" | \"empty\"; parent: TreeNode; level: number; ancestors: string[] };\n\nconst ROW = 28;\nconst INDENT = 16;\n\nexport function TreeView({\n  items,\n  expanded: expandedProp,\n  defaultExpanded = [],\n  onExpandedChange,\n  selected: selectedProp,\n  defaultSelected = [],\n  onSelectedChange,\n  selectionMode = \"single\",\n  selectionFollowsFocus = false,\n  expandOnClick = true,\n  onAction,\n  loadChildren,\n  indentGuides = true,\n  className,\n  onKeyDown,\n  ...rest\n}: TreeViewProps) {\n  const reduce = !!useReducedMotion();\n  const [expanded, setExpanded] = useControllableState({ value: expandedProp, defaultValue: defaultExpanded, onChange: onExpandedChange });\n  const [selected, setSelected] = useControllableState({ value: selectedProp, defaultValue: defaultSelected, onChange: onSelectedChange });\n  const [loaded, setLoaded] = useState<Record<string, TreeNode[] | \"loading\" | \"error\">>({});\n  const [focusId, setFocusId] = useState<string | null>(null);\n  const [anchor, setAnchor] = useState<string | null>(null);\n  // Changes made from the keyboard render on the same frame; pointer changes animate.\n  const [instant, setInstant] = useState(false);\n  const [announce, setAnnounce] = useState(\"\");\n  const rowEls = useRef(new Map<string, HTMLDivElement>());\n  const moveFocus = useRef(false);\n  const typed = useRef({ text: \"\", at: 0 });\n  const treeRef = useRef<HTMLDivElement>(null);\n  const id = useId();\n\n  const openSet = useMemo(() => new Set(expanded), [expanded]);\n  const selectedSet = useMemo(() => new Set(selected), [selected]);\n\n  const childrenOf = useCallback(\n    (node: TreeNode): TreeNode[] | \"loading\" | \"error\" | undefined => {\n      if (node.children) return node.children;\n      if (!node.hasChildren) return undefined;\n      return loaded[node.id] ?? \"loading\";\n    },\n    [loaded],\n  );\n\n  // The tree, flattened to the rows you can see. One flat list keeps the keyboard model,\n  // the hover highlight and the height animation simple, and is how large trees virtualise.\n  const rows = useMemo(() => {\n    const out: Row[] = [];\n    const walk = (nodes: TreeNode[], level: number, parent: string | null, ancestors: string[]) => {\n      nodes.forEach((node, i) => {\n        const kids = childrenOf(node);\n        const branch = kids !== undefined;\n        const open = branch && openSet.has(node.id);\n        out.push({ kind: \"node\", node, level, parent, ancestors, pos: i + 1, size: nodes.length, branch, open });\n        if (!open) return;\n        const next = [...ancestors, node.id];\n        if (kids === \"loading\" || kids === \"error\") out.push({ kind: \"status\", status: kids, parent: node, level: level + 1, ancestors: next });\n        else if (kids!.length === 0) out.push({ kind: \"status\", status: \"empty\", parent: node, level: level + 1, ancestors: next });\n        else walk(kids!, level + 1, node.id, next);\n      });\n    };\n    walk(items, 1, null, []);\n    return out;\n  }, [items, openSet, childrenOf]);\n\n  const nodeRows = rows.filter((r): r is Extract<Row, { kind: \"node\" }> => r.kind === \"node\");\n  const byId = new Map(nodeRows.map((r) => [r.node.id, r]));\n\n  // If the focused row was folded away, focus falls back to its nearest visible ancestor.\n  let current = focusId && byId.has(focusId) ? focusId : null;\n  if (focusId && !current) {\n    const lost = findPath(items, focusId, childrenOf);\n    current = lost?.reverse().find((a) => byId.has(a)) ?? null;\n  }\n  const tabStop = current ?? selected.find((s) => byId.has(s)) ?? nodeRows[0]?.node.id ?? null;\n\n  // The branch holding the selection (or focus) lights its guides.\n  const litTarget = byId.get(selected[0] ?? \"\") ?? byId.get(current ?? \"\");\n  const lit = new Set(litTarget?.ancestors ?? []);\n\n  useEffect(() => {\n    if (!moveFocus.current || !tabStop) return;\n    moveFocus.current = false;\n    rowEls.current.get(tabStop)?.focus();\n  }, [tabStop]);\n\n  /* ---------------------------------------------------------------- actions */\n\n  const load = useCallback(\n    (node: TreeNode) => {\n      if (!loadChildren || node.children || !node.hasChildren) return;\n      const state = loaded[node.id];\n      if (state && state !== \"error\") return;\n      setLoaded((l) => ({ ...l, [node.id]: \"loading\" }));\n      setAnnounce(`Loading ${node.label}`);\n      loadChildren(node).then(\n        (kids) => {\n          setLoaded((l) => ({ ...l, [node.id]: kids }));\n          setAnnounce(kids.length ? `${node.label}, ${kids.length} ${kids.length === 1 ? \"item\" : \"items\"}` : `${node.label} is empty`);\n        },\n        () => {\n          setLoaded((l) => ({ ...l, [node.id]: \"error\" }));\n          setAnnounce(`Couldn't load ${node.label}. Press Right arrow to try again.`);\n        },\n      );\n    },\n    [loadChildren, loaded],\n  );\n\n  const setOpen = (node: TreeNode, open: boolean) => {\n    if (open) load(node);\n    if (open === openSet.has(node.id)) return;\n    setExpanded(open ? [...expanded, node.id] : expanded.filter((e) => e !== node.id));\n  };\n\n  const select = (nodeId: string, mode: \"replace\" | \"toggle\" | \"range\") => {\n    if (selectionMode === \"none\") return;\n    const node = byId.get(nodeId)?.node;\n    if (!node || node.disabled) return;\n    if (selectionMode === \"single\" || mode === \"replace\") {\n      setSelected([nodeId]);\n      setAnchor(nodeId);\n    } else if (mode === \"toggle\") {\n      setSelected(selectedSet.has(nodeId) ? selected.filter((s) => s !== nodeId) : [...selected, nodeId]);\n      setAnchor(nodeId);\n    } else {\n      const from = nodeRows.findIndex((r) => r.node.id === (anchor ?? nodeId));\n      const to = nodeRows.findIndex((r) => r.node.id === nodeId);\n      const [a, b] = from < to ? [from, to] : [to, from];\n      setSelected(nodeRows.slice(a, b + 1).filter((r) => !r.node.disabled).map((r) => r.node.id));\n    }\n  };\n\n  const focusRow = (nodeId: string | undefined, fromKeyboard: boolean) => {\n    if (!nodeId) return;\n    moveFocus.current = true;\n    setFocusId(nodeId);\n    if (fromKeyboard && nodeId === tabStop) rowEls.current.get(nodeId)?.focus();\n  };\n\n  /* ---------------------------------------------------------------- keyboard (APG tree view) */\n\n  const handleKey = (e: React.KeyboardEvent<HTMLDivElement>) => {\n    onKeyDown?.(e);\n    if (e.defaultPrevented || !tabStop) return;\n    const row = byId.get(tabStop);\n    if (!row) return;\n    const index = nodeRows.indexOf(row);\n    const { node } = row;\n    const multi = selectionMode === \"multiple\";\n    setInstant(true);\n\n    const go = (target: (typeof nodeRows)[number] | undefined) => {\n      if (!target) return;\n      focusRow(target.node.id, true);\n      if (multi && e.shiftKey) select(target.node.id, \"range\");\n      else if (selectionMode === \"single\" && selectionFollowsFocus) select(target.node.id, \"replace\");\n    };\n\n    switch (e.key) {\n      case \"ArrowDown\":\n        go(nodeRows[index + 1]);\n        break;\n      case \"ArrowUp\":\n        go(nodeRows[index - 1]);\n        break;\n      case \"Home\":\n        go(nodeRows[0]);\n        break;\n      case \"End\":\n        go(nodeRows[nodeRows.length - 1]);\n        break;\n      case \"ArrowRight\":\n        if (!row.branch) return;\n        if (!row.open || loaded[node.id] === \"error\") setOpen(node, true);\n        else go(nodeRows[index + 1]?.parent === node.id ? nodeRows[index + 1] : undefined);\n        break;\n      case \"ArrowLeft\":\n        if (row.branch && row.open) setOpen(node, false);\n        else go(row.parent ? byId.get(row.parent) : undefined);\n        break;\n      case \"Enter\":\n        if (row.branch) setOpen(node, !row.open);\n        else if (!node.disabled) {\n          select(node.id, \"replace\");\n          onAction?.(node);\n        }\n        break;\n      case \" \":\n        select(node.id, multi ? \"toggle\" : \"replace\");\n        break;\n      case \"*\": {\n        // Opens every sibling folder at this level, per the tree pattern.\n        const siblings = nodeRows.filter((r) => r.parent === row.parent && r.branch && !r.open);\n        siblings.forEach((s) => load(s.node));\n        setExpanded([...expanded, ...siblings.map((s) => s.node.id)]);\n        break;\n      }\n      case \"a\":\n        if (!(multi && (e.metaKey || e.ctrlKey))) return typeahead(e);\n        setSelected(nodeRows.filter((r) => !r.node.disabled).map((r) => r.node.id));\n        break;\n      default:\n        return typeahead(e);\n    }\n    e.preventDefault();\n  };\n\n  // Typing jumps to the next visible row whose label starts with what you typed.\n  const typeahead = (e: React.KeyboardEvent) => {\n    if (e.key.length !== 1 || e.metaKey || e.ctrlKey || e.altKey) return;\n    const now = e.timeStamp;\n    const t = typed.current;\n    t.text = now - t.at > 500 ? e.key.toLowerCase() : t.text + e.key.toLowerCase();\n    t.at = now;\n    const start = nodeRows.findIndex((r) => r.node.id === tabStop);\n    // A repeated single letter cycles through matches; a longer string refines the current one.\n    const repeated = t.text.length > 1 && [...t.text].every((c) => c === t.text[0]);\n    const needle = repeated ? t.text[0] : t.text;\n    const from = needle.length === 1 ? start + 1 : start;\n    for (let i = 0; i < nodeRows.length; i++) {\n      const r = nodeRows[(from + i) % nodeRows.length];\n      if (r.node.label.toLowerCase().startsWith(needle)) {\n        focusRow(r.node.id, true);\n        if (selectionMode === \"single\" && selectionFollowsFocus) select(r.node.id, \"replace\");\n        break;\n      }\n    }\n    e.preventDefault();\n  };\n\n  /* ---------------------------------------------------------------- render */\n\n  const { onMove, onLeave, glide } = useGlide(treeRef, reduce);\n  const enter = { duration: instant ? 0 : reduce ? 0.12 : 0.2, ease: ease.out };\n\n  return (\n    <div\n      ref={treeRef}\n      role=\"tree\"\n      aria-multiselectable={selectionMode === \"multiple\" || undefined}\n      onKeyDown={handleKey}\n      onPointerDown={() => setInstant(false)}\n      onPointerMove={onMove}\n      onPointerLeave={onLeave}\n      className={cn(\"relative isolate overflow-y-auto overscroll-contain p-1 text-[13px] outline-none select-none\", className)}\n      {...rest}\n    >\n      <motion.div aria-hidden style={glide} className=\"pointer-events-none absolute inset-x-1 top-0 -z-10 rounded-md bg-fg/[0.04]\" />\n      <AnimatePresence initial={false}>\n        {rows.map((row) => {\n          const key = row.kind === \"node\" ? row.node.id : `${row.parent.id}:${row.status}`;\n          return (\n            <motion.div\n              key={key}\n              // Rows open by height, so everything below slides down instead of jumping.\n              initial={{ height: 0, opacity: 0 }}\n              animate={{ height: ROW, opacity: 1 }}\n              exit={{ height: 0, opacity: 0, transition: { ...enter, duration: instant ? 0 : reduce ? 0.1 : 0.15 } }}\n              transition={reduce ? { height: { duration: 0 }, opacity: enter } : enter}\n              className=\"overflow-hidden\"\n              role=\"none\"\n            >\n              {row.kind === \"node\" ? (\n                <TreeRow\n                  row={row}\n                  selected={selectedSet.has(row.node.id)}\n                  tabbable={row.node.id === tabStop}\n                  lit={lit}\n                  guides={indentGuides}\n                  instant={instant}\n                  failed={loaded[row.node.id] === \"error\"}\n                  busy={row.open && childrenOf(row.node) === \"loading\"}\n                  ref={(el) => {\n                    if (el) rowEls.current.set(row.node.id, el);\n                    else rowEls.current.delete(row.node.id);\n                  }}\n                  onFocus={() => setFocusId(row.node.id)}\n                  onToggle={() => setOpen(row.node, !row.open)}\n                  onPress={(e) => {\n                    setFocusId(row.node.id);\n                    if (row.node.disabled) return;\n                    if (selectionMode === \"multiple\" && (e.metaKey || e.ctrlKey)) select(row.node.id, \"toggle\");\n                    else if (selectionMode === \"multiple\" && e.shiftKey) select(row.node.id, \"range\");\n                    else select(row.node.id, \"replace\");\n                    if (row.branch && expandOnClick && !e.shiftKey && !e.metaKey && !e.ctrlKey) setOpen(row.node, !row.open);\n                  }}\n                  onDoubleClick={() => !row.branch && !row.node.disabled && onAction?.(row.node)}\n                />\n              ) : (\n                <StatusRow row={row} lit={lit} guides={indentGuides} onRetry={() => load(row.parent)} />\n              )}\n            </motion.div>\n          );\n        })}\n      </AnimatePresence>\n      {rows.length === 0 && <p className=\"px-2 py-1.5 text-[12.5px] text-fg-3\">No files</p>}\n      <span id={`${id}-status`} role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n        {announce}\n      </span>\n    </div>\n  );\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Rows\n * -----------------------------------------------------------------------------------------------*/\n\ntype NodeRow = Extract<Row, { kind: \"node\" }>;\n\nfunction TreeRow({\n  row,\n  selected,\n  tabbable,\n  lit,\n  guides,\n  instant,\n  failed,\n  busy,\n  ref,\n  onFocus,\n  onToggle,\n  onPress,\n  onDoubleClick,\n}: {\n  row: NodeRow;\n  selected: boolean;\n  tabbable: boolean;\n  lit: Set<string>;\n  guides: boolean;\n  instant: boolean;\n  failed: boolean;\n  busy: boolean;\n  ref: React.Ref<HTMLDivElement>;\n  onFocus: () => void;\n  onToggle: () => void;\n  onPress: (e: React.MouseEvent) => void;\n  onDoubleClick: () => void;\n}) {\n  const { node, level, branch, open } = row;\n  return (\n    <div\n      ref={ref}\n      role=\"treeitem\"\n      aria-level={level}\n      aria-posinset={row.pos}\n      aria-setsize={row.size}\n      aria-expanded={branch ? open : undefined}\n      aria-selected={selected}\n      aria-disabled={node.disabled || undefined}\n      aria-busy={busy || undefined}\n      tabIndex={tabbable ? 0 : -1}\n      data-row\n      data-selected={selected || undefined}\n      onFocus={onFocus}\n      onClick={onPress}\n      onDoubleClick={onDoubleClick}\n      style={{ paddingLeft: 6 + (level - 1) * INDENT }}\n      className={cn(\n        \"group/row relative flex h-7 cursor-default items-center gap-1.5 rounded-md pr-2 text-fg-2 outline-none\",\n        \"transition-[background-color,color] duration-100\",\n        \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:-outline-offset-1 focus-visible:outline-fg-3\",\n        selected && \"bg-fg/[0.08] text-fg\",\n        node.disabled && \"text-fg-4\",\n      )}\n    >\n      {guides && <Guides row={row} lit={lit} />}\n      <span\n        aria-hidden\n        onClick={(e) => {\n          if (!branch) return;\n          e.stopPropagation();\n          onToggle();\n        }}\n        className={cn(\"relative grid size-4 shrink-0 place-items-center text-fg-4\", branch && \"hover:text-fg-2 pointer-coarse:after:absolute pointer-coarse:after:-inset-2.5\")}\n      >\n        {branch &&\n          (busy ? (\n            <Spinner />\n          ) : (\n            <ChevronRight\n              size={14}\n              className={cn(\"transition-transform duration-180 ease-out-expo motion-reduce:transition-none\", open && \"rotate-90\", instant && \"duration-0\")}\n            />\n          ))}\n      </span>\n      <span aria-hidden className={cn(\"flex shrink-0 [&_svg]:size-4\", selected ? \"text-fg-2\" : \"text-fg-3\", failed && \"text-danger\")}>\n        {node.icon ?? (branch ? <FolderGlyph open={open} /> : <FileGlyph />)}\n      </span>\n      <span className=\"min-w-0 flex-1 truncate\">{node.label}</span>\n      {failed ? (\n        <span className=\"shrink-0 text-[11.5px] text-danger\">Couldn’t load</span>\n      ) : (\n        node.meta != null && <span className=\"shrink-0 font-mono text-2xs text-fg-4 tabular\">{node.meta}</span>\n      )}\n    </div>\n  );\n}\n\nfunction StatusRow({ row, lit, guides, onRetry }: { row: Extract<Row, { kind: \"status\" }>; lit: Set<string>; guides: boolean; onRetry: () => void }) {\n  return (\n    <div\n      aria-hidden\n      onClick={row.status === \"error\" ? onRetry : undefined}\n      style={{ paddingLeft: 6 + (row.level - 1) * INDENT + 22 }}\n      className={cn(\"relative flex h-7 items-center gap-1.5 pr-2 text-[12.5px] text-fg-4\", row.status === \"error\" && \"cursor-pointer text-fg-3 hover:text-fg\")}\n    >\n      {guides && <Guides row={row} lit={lit} />}\n      {row.status === \"loading\" && <span className=\"h-2 w-24 animate-pulse-soft rounded-full bg-hover motion-reduce:animate-none\" />}\n      {row.status === \"empty\" && \"Empty folder\"}\n      {row.status === \"error\" && (\n        <>\n          Couldn’t load <span className=\"text-fg underline decoration-fg-4 underline-offset-2\">Try again</span>\n        </>\n      )}\n    </div>\n  );\n}\n\n// One hairline per ancestor level, centered under that ancestor's chevron.\nfunction Guides({ row, lit }: { row: { level: number; ancestors: string[] }; lit: Set<string> }) {\n  return (\n    <>\n      {row.ancestors.map((a, i) => (\n        <span\n          key={a}\n          aria-hidden\n          style={{ left: 6 + i * INDENT + 7.5 }}\n          className={cn(\"pointer-events-none absolute inset-y-0 w-px transition-colors duration-200\", lit.has(a) ? \"bg-fg-4\" : \"bg-line-2\")}\n        />\n      ))}\n    </>\n  );\n}\n\n/* -------------------------------------------------------------------------------------------------\n * The hover highlight that glides between rows\n * -----------------------------------------------------------------------------------------------*/\n\nfunction useGlide(treeRef: React.RefObject<HTMLDivElement | null>, reduce: boolean) {\n  const y = useMotionValue(0);\n  const opacity = useMotionValue(0);\n  const shown = useRef(false);\n\n  const onMove = (e: React.PointerEvent) => {\n    if (e.pointerType !== \"mouse\") return;\n    const row = (e.target as HTMLElement).closest<HTMLElement>(\"[data-row]\");\n    if (!row || !treeRef.current?.contains(row) || row.hasAttribute(\"data-selected\")) {\n      if (shown.current) animate(opacity, 0, { duration: 0.12 });\n      shown.current = false;\n      return;\n    }\n    const top = row.offsetTop;\n    if (!shown.current || reduce) {\n      y.jump(top);\n      animate(opacity, 1, { duration: 0.08 });\n    } else animate(y, top, spring.follow);\n    shown.current = true;\n  };\n  const onLeave = () => {\n    shown.current = false;\n    animate(opacity, 0, { duration: 0.15 });\n  };\n  useEffect(() => () => opacity.stop(), [opacity]);\n\n  return { onMove, onLeave, glide: { y, opacity, height: ROW } };\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Helpers and glyphs\n * -----------------------------------------------------------------------------------------------*/\n\nfunction findPath(nodes: TreeNode[], target: string, childrenOf: (n: TreeNode) => TreeNode[] | \"loading\" | \"error\" | undefined, trail: string[] = []): string[] | null {\n  for (const n of nodes) {\n    if (n.id === target) return trail;\n    const kids = childrenOf(n);\n    if (Array.isArray(kids)) {\n      const found = findPath(kids, target, childrenOf, [...trail, n.id]);\n      if (found) return found;\n    }\n  }\n  return null;\n}\n\nfunction FolderGlyph({ open }: { open: boolean }) {\n  return (\n    <svg width={16} height={16} viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.4} strokeLinejoin=\"round\" aria-hidden>\n      {open ? (\n        <path d=\"M2.5 11.75v-7.5c0-.4.35-.75.75-.75h3l1.5 1.5h4.5c.4 0 .75.35.75.75V7M2.5 11.75 4.2 7.6a1 1 0 0 1 .93-.6h8.37a.5.5 0 0 1 .46.7l-1.6 3.9a1 1 0 0 1-.93.65H3.25a.75.75 0 0 1-.75-.5z\" />\n      ) : (\n        <path d=\"M2.5 4.25c0-.4.35-.75.75-.75h3l1.5 1.5h5c.4 0 .75.35.75.75v6.5c0 .4-.35.75-.75.75h-9.5a.75.75 0 0 1-.75-.75z\" />\n      )}\n    </svg>\n  );\n}\n\nfunction FileGlyph() {\n  return (\n    <svg width={16} height={16} viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.4} strokeLinejoin=\"round\" aria-hidden>\n      <path d=\"M4 2.5h5l3.5 3.5v7.5H4zM9 2.5V6h3.5\" />\n    </svg>\n  );\n}\n\nfunction Spinner() {\n  return (\n    <svg width={12} height={12} viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.6} strokeLinecap=\"round\" className=\"animate-spin-slow motion-reduce:animate-none\" aria-hidden>\n      <path d=\"M8 2.25a5.75 5.75 0 1 0 5.75 5.75\" />\n    </svg>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/tree-view"
}
