{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "anatomy-tree",
  "title": "Anatomy tree",
  "description": "Search, select and control visibility in hierarchical anatomy data.",
  "dependencies": [
    "@base-ui/react@^1.8.0",
    "class-variance-authority@^0.7.1",
    "cn@^0.5.0"
  ],
  "registryDependencies": [
    "button",
    "empty",
    "field",
    "input"
  ],
  "files": [
    {
      "path": "registry/base-nova/anatomy-tree/anatomy-tree.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Button as ButtonPrimitive } from \"@base-ui/react/button\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"cn\"\n\nimport { Button } from \"@/components/ui/button\"\nimport { Empty, EmptyDescription, EmptyHeader } from \"@/components/ui/empty\"\nimport { Field, FieldGroup, FieldLabel } from \"@/components/ui/field\"\nimport { Input } from \"@/components/ui/input\"\n\n// These controls belong to the tree row: its hover and selection surface\n// stays on the treeitem, independent of the consumer's Button variants.\nconst treeButtonVariants = cva(\n  \"inline-flex shrink-0 items-center justify-center rounded-md text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50\",\n  {\n    variants: {\n      variant: {\n        expand: \"size-8 hover:bg-accent hover:text-accent-foreground\",\n        label:\n          \"min-w-0 flex-1 justify-start gap-2 py-2 text-start font-normal break-words whitespace-normal group-aria-selected/row:font-semibold\",\n      },\n    },\n  }\n)\n\nfunction TreeButton({\n  variant,\n  ...props\n}: Omit<ButtonPrimitive.Props, \"className\"> &\n  VariantProps<typeof treeButtonVariants>) {\n  return (\n    <ButtonPrimitive className={treeButtonVariants({ variant })} {...props} />\n  )\n}\n\nexport type AnatomyNode = {\n  id: string\n  label: string\n  children?: readonly AnatomyNode[]\n  /** A CSS color shown as a decorative swatch before the label. */\n  color?: string\n  /**\n   * Defaults to true. A row that is not selectable expands or collapses\n   * instead, for groupings that have no single structure to select.\n   */\n  selectable?: boolean\n}\nexport type AnatomyTreeProps = {\n  data: readonly AnatomyNode[]\n  selectedId: string | null\n  onSelectionChange: (id: string) => void\n  /** Missing IDs are visible. Branches summarize their leaf descendants. */\n  visibility: Readonly<Record<string, boolean>>\n  onVisibilityChange: (visibility: Record<string, boolean>) => void\n  defaultExpandedIds?: readonly string[]\n  label?: string\n  className?: string\n}\nexport type AnatomyIndex = {\n  nodes: Map<string, AnatomyNode>\n  parents: Map<string, string>\n  leaves: Map<string, string[]>\n}\nexport type AnatomyRow = {\n  node: AnatomyNode\n  level: number\n  size: number\n  position: number\n}\n\nexport function anatomyIndex(data: readonly AnatomyNode[]): AnatomyIndex {\n  const nodes = new Map<string, AnatomyNode>()\n  const parents = new Map<string, string>()\n  const leaves = new Map<string, string[]>()\n  function visit(items: readonly AnatomyNode[], parent?: string): string[] {\n    return items.flatMap((node) => {\n      if (!node.id)\n        throw new Error(\"Anatomy IDs must be nonempty and unique: found empty\")\n      if (nodes.has(node.id))\n        throw new Error(\n          `Anatomy IDs must be nonempty and unique: \"${node.id}\" repeats`\n        )\n      nodes.set(node.id, node)\n      if (parent) parents.set(node.id, parent)\n      const ids = node.children?.length\n        ? visit(node.children, node.id)\n        : [node.id]\n      leaves.set(node.id, ids)\n      return ids\n    })\n  }\n  visit(data)\n  return { nodes, parents, leaves }\n}\n\nexport function leafIds(node: AnatomyNode): string[] {\n  return node.children?.length ? node.children.flatMap(leafIds) : [node.id]\n}\n\nfunction leafState(\n  ids: readonly string[],\n  visibility: Readonly<Record<string, boolean>>\n): boolean | \"mixed\" {\n  const values = ids.map((id) => visibility[id] !== false)\n  return values.every(Boolean) ? true : values.some(Boolean) ? \"mixed\" : false\n}\n\nexport function visibilityState(\n  node: AnatomyNode,\n  visibility: Readonly<Record<string, boolean>>\n): boolean | \"mixed\" {\n  return leafState(leafIds(node), visibility)\n}\n\n/** Hides every leaf of a fully visible node, otherwise shows them all. */\nexport function nextVisibility(\n  node: AnatomyNode,\n  index: AnatomyIndex,\n  visibility: Readonly<Record<string, boolean>>\n): Record<string, boolean> {\n  // A Map keeps IDs such as \"__proto__\" as ordinary keys.\n  const next = new Map<string, boolean>()\n  for (const id of index.nodes.keys())\n    if (Object.hasOwn(visibility, id)) next.set(id, visibility[id]!)\n  const ids = index.leaves.get(node.id) ?? leafIds(node)\n  const value = leafState(ids, visibility) !== true\n  for (const id of ids) next.set(id, value)\n  return Object.fromEntries(next)\n}\n\n/** Visible rows in order. A search shows matches with their ancestors. */\nexport function anatomyRows(\n  data: readonly AnatomyNode[],\n  index: AnatomyIndex,\n  expanded: ReadonlySet<string>,\n  query: string\n): AnatomyRow[] {\n  const term = query.trim().toLocaleLowerCase()\n  const included = term ? new Set<string>() : undefined\n  if (included)\n    for (const node of index.nodes.values()) {\n      if (!node.label.toLocaleLowerCase().includes(term)) continue\n      let id: string | undefined = node.id\n      while (id && !included.has(id)) {\n        included.add(id)\n        id = index.parents.get(id)\n      }\n    }\n  const rows: AnatomyRow[] = []\n  function visit(items: readonly AnatomyNode[], level: number) {\n    const visible = included ? items.filter((n) => included.has(n.id)) : items\n    visible.forEach((node, position) => {\n      rows.push({ node, level, size: visible.length, position: position + 1 })\n      if (included || expanded.has(node.id))\n        visit(node.children ?? [], level + 1)\n    })\n  }\n  visit(data, 1)\n  return rows\n}\n\n/** The roving tab stop: the active row, else its nearest visible ancestor. */\nexport function focusTarget(\n  rows: readonly AnatomyRow[],\n  activePath: readonly string[],\n  parents: ReadonlyMap<string, string>\n): string | undefined {\n  const ids = new Set(rows.map(({ node }) => node.id))\n  let id: string | undefined = activePath[0]\n  while (id && !ids.has(id)) id = parents.get(id)\n  return id ?? activePath.find((path) => ids.has(path)) ?? rows[0]?.node.id\n}\n\n/**\n * Brings a row into view inside its nearest scrolling container only. Unlike\n * scrollIntoView, the page itself never moves, so a pick in a viewer does not\n * jump a phone layout to the tree.\n */\nfunction scrollWithinContainer(row: HTMLElement) {\n  let container = row.parentElement\n  while (container) {\n    const { overflowY } = getComputedStyle(container)\n    if (\n      /auto|scroll/.test(overflowY) &&\n      container.scrollHeight > container.clientHeight\n    )\n      break\n    container = container.parentElement\n  }\n  if (!container) return\n  const bounds = container.getBoundingClientRect()\n  const target = row.getBoundingClientRect()\n  if (target.top < bounds.top) container.scrollTop -= bounds.top - target.top\n  else if (target.bottom > bounds.bottom)\n    container.scrollTop += Math.min(\n      target.bottom - bounds.bottom,\n      target.top - bounds.top\n    )\n}\n\nexport function AnatomyTree({\n  data,\n  selectedId,\n  onSelectionChange,\n  visibility,\n  onVisibilityChange,\n  defaultExpandedIds = [],\n  label = \"Anatomy\",\n  className,\n}: AnatomyTreeProps) {\n  const index = React.useMemo(() => anatomyIndex(data), [data])\n  const [expanded, setExpanded] = React.useState(\n    () => new Set(defaultExpandedIds)\n  )\n  const [query, setQuery] = React.useState(\"\")\n  // Typing stays responsive while larger trees filter.\n  const deferredQuery = React.useDeferredValue(query)\n  const [activePath, setActivePath] = React.useState<string[]>(() =>\n    data[0] ? [data[0].id] : []\n  )\n  const search = React.useRef<HTMLInputElement>(null)\n  const lastFocused = React.useRef<Element | null>(null)\n  const refs = React.useRef(new Map<string, HTMLDivElement>())\n  const helpId = React.useId()\n  const searchId = React.useId()\n  const searching = deferredQuery.trim() !== \"\"\n  const groupsExpand = React.useMemo(\n    () => [...index.nodes.values()].some((node) => node.selectable === false),\n    [index]\n  )\n  const rows = React.useMemo(\n    () => anatomyRows(data, index, expanded, deferredQuery),\n    [data, index, expanded, deferredQuery]\n  )\n  const focusId = focusTarget(rows, activePath, index.parents)\n  // Reveal a selection made outside the tree, such as in a 3D view: when its\n  // row is not shown, expand its ancestors, then scroll the row into view.\n  const [reveal, setReveal] = React.useState({ id: selectedId, scroll: false })\n  if (reveal.id !== selectedId) {\n    const known = !!selectedId && index.nodes.has(selectedId)\n    const shown = rows.some(({ node }) => node.id === selectedId)\n    setReveal({ id: selectedId, scroll: known })\n    const ancestors: string[] = []\n    let parent = known && !shown ? index.parents.get(selectedId!) : undefined\n    while (parent) {\n      ancestors.push(parent)\n      parent = index.parents.get(parent)\n    }\n    if (ancestors.some((id) => !expanded.has(id)))\n      setExpanded(new Set([...expanded, ...ancestors]))\n  }\n  React.useEffect(() => {\n    const row = reveal.scroll && reveal.id && refs.current.get(reveal.id)\n    if (row) scrollWithinContainer(row)\n  }, [reveal])\n  React.useLayoutEffect(() => {\n    // Recover only when an update removed the focused element. Focus that\n    // left the tree on purpose, even to the body, is never taken back.\n    const previous = lastFocused.current\n    if (!previous || previous.isConnected) return\n    lastFocused.current = null\n    if (document.activeElement && document.activeElement !== document.body)\n      return\n    if (focusId) refs.current.get(focusId)?.focus()\n    else search.current?.focus()\n  })\n  function focus(id?: string) {\n    if (id) {\n      refs.current.get(id)?.focus()\n    }\n  }\n  function toggleExpanded(id: string) {\n    setExpanded((previous) => {\n      const next = new Set(previous)\n      if (next.has(id)) next.delete(id)\n      else next.add(id)\n      return next\n    })\n  }\n  function activate(node: AnatomyNode) {\n    if (node.selectable !== false) onSelectionChange(node.id)\n    else if (node.children?.length && !searching) toggleExpanded(node.id)\n  }\n  function toggleVisibility(node: AnatomyNode) {\n    onVisibilityChange(nextVisibility(node, index, visibility))\n  }\n  return (\n    <section\n      className={cn(\n        \"rounded-xl border bg-background p-4 text-foreground\",\n        className\n      )}\n    >\n      <FieldGroup className=\"mb-3\">\n        <Field>\n          <FieldLabel htmlFor={searchId}>\n            Search {label.toLocaleLowerCase()}\n          </FieldLabel>\n          <Input\n            ref={search}\n            id={searchId}\n            type=\"search\"\n            value={query}\n            onChange={(event) => setQuery(event.target.value)}\n          />\n        </Field>\n      </FieldGroup>\n      <p id={helpId} className=\"mb-3 text-xs text-muted-foreground\">\n        {groupsExpand\n          ? \"Arrows navigate and expand. Enter selects, or expands a group. Space toggles visibility.\"\n          : \"Arrows navigate and expand. Enter selects. Space toggles visibility.\"}\n      </p>\n      <div\n        role=\"tree\"\n        aria-label={label}\n        aria-describedby={helpId}\n        onFocusCapture={(event) => {\n          lastFocused.current = event.target\n        }}\n        onBlurCapture={(event) => {\n          const element = event.target\n          if (\n            event.relatedTarget instanceof Node &&\n            event.currentTarget.contains(event.relatedTarget)\n          )\n            return\n          // Removing the focused row can also blur it. Once the update has\n          // committed, an element that is still attached was left on purpose.\n          queueMicrotask(() => {\n            if (element.isConnected && lastFocused.current === element)\n              lastFocused.current = null\n          })\n        }}\n      >\n        {rows.map(({ node, level, size, position }, rowIndex) => {\n          const branch = !!node.children?.length\n          const open = searching || expanded.has(node.id)\n          const checked = leafState(index.leaves.get(node.id)!, visibility)\n          const state =\n            checked === \"mixed\" ? \"Mixed\" : checked ? \"Visible\" : \"Hidden\"\n          return (\n            <div\n              key={node.id}\n              ref={(element) => {\n                if (element) refs.current.set(node.id, element)\n                else refs.current.delete(node.id)\n              }}\n              role=\"treeitem\"\n              // Voice control users say what they see, so the name carries the\n              // row's visible text: the label, then its visibility state.\n              aria-label={`${node.label}, ${state.toLowerCase()}`}\n              aria-level={level}\n              aria-setsize={size}\n              aria-posinset={position}\n              aria-expanded={branch ? open : undefined}\n              aria-selected={\n                node.selectable === false ? undefined : selectedId === node.id\n              }\n              aria-checked={checked}\n              tabIndex={focusId === node.id ? 0 : -1}\n              onFocus={() => {\n                const path = [node.id]\n                let parent = index.parents.get(node.id)\n                while (parent) {\n                  path.push(parent)\n                  parent = index.parents.get(parent)\n                }\n                setActivePath(path)\n              }}\n              onKeyDown={(event) => {\n                if (\n                  ![\n                    \"ArrowDown\",\n                    \"ArrowUp\",\n                    \"ArrowRight\",\n                    \"ArrowLeft\",\n                    \"Home\",\n                    \"End\",\n                    \"Enter\",\n                    \" \",\n                  ].includes(event.key)\n                )\n                  return\n                event.preventDefault()\n                if (event.key === \"ArrowDown\")\n                  focus(rows[rowIndex + 1]?.node.id)\n                if (event.key === \"ArrowUp\") focus(rows[rowIndex - 1]?.node.id)\n                if (event.key === \"Home\") focus(rows[0]?.node.id)\n                if (event.key === \"End\") focus(rows.at(-1)?.node.id)\n                if (event.key === \"ArrowRight\" && branch) {\n                  if (!open) toggleExpanded(node.id)\n                  else if (rows[rowIndex + 1]?.level === level + 1)\n                    focus(rows[rowIndex + 1]?.node.id)\n                }\n                if (event.key === \"ArrowLeft\") {\n                  if (branch && open && !searching) toggleExpanded(node.id)\n                  else focus(index.parents.get(node.id))\n                }\n                if (event.key === \"Enter\") activate(node)\n                if (event.key === \" \") toggleVisibility(node)\n              }}\n              className=\"group/row my-1 flex min-h-10 items-center gap-1 rounded-md px-2 text-sm outline-offset-2 hover:bg-muted focus-visible:outline-2 focus-visible:outline-ring aria-selected:bg-accent aria-selected:font-semibold aria-selected:text-accent-foreground\"\n              style={{ paddingInlineStart: `${(level - 1) * 1.25 + 0.5}rem` }}\n            >\n              {branch ? (\n                <TreeButton\n                  variant=\"expand\"\n                  tabIndex={-1}\n                  aria-label={`${open ? \"Collapse\" : \"Expand\"} ${node.label}`}\n                  disabled={searching}\n                  onClick={() => {\n                    focus(node.id)\n                    toggleExpanded(node.id)\n                  }}\n                >\n                  {open ? \"−\" : \"+\"}\n                </TreeButton>\n              ) : (\n                <span className=\"w-8 shrink-0\" />\n              )}\n              <TreeButton\n                variant=\"label\"\n                tabIndex={-1}\n                onClick={() => {\n                  focus(node.id)\n                  activate(node)\n                }}\n              >\n                {node.color && (\n                  <span\n                    aria-hidden=\"true\"\n                    className=\"size-2.5 shrink-0 rounded-full\"\n                    style={{ backgroundColor: node.color }}\n                  />\n                )}\n                <span className=\"min-w-0\">{node.label}</span>\n              </TreeButton>{\" \"}\n              <Button\n                variant=\"outline\"\n                size=\"xs\"\n                tabIndex={-1}\n                // Starts with the visible state so voice control can target it.\n                aria-label={`${state}: ${checked === true ? \"hide\" : \"show\"} ${node.label}`}\n                onClick={() => {\n                  focus(node.id)\n                  toggleVisibility(node)\n                }}\n              >\n                {state}\n              </Button>\n            </div>\n          )\n        })}\n      </div>\n      {!rows.length && (\n        <Empty role=\"status\">\n          <EmptyHeader>\n            <EmptyDescription>\n              {data.length ? \"No matching anatomy.\" : \"No anatomy available.\"}\n            </EmptyDescription>\n          </EmptyHeader>\n        </Empty>\n      )}\n    </section>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "Anatomy tree docs: https://organui.com/docs/components/anatomy-tree",
  "categories": [
    "anatomy"
  ],
  "type": "registry:ui"
}