{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tree",
  "type": "registry:ui",
  "title": "Tree",
  "description": "Accessible tree view with roving keyboard navigation, single/multi selection, and animated disclosure.",
  "dependencies": [
    "@base-ui/react@^1.7.0",
    "lucide-react@^1.31.0"
  ],
  "registryDependencies": [
    "https://control-ui.dev/r/core.json",
    "https://control-ui.dev/r/track-highlight.json"
  ],
  "files": [
    {
      "path": "src/registry/knob-contracts/tree-knobs.ts",
      "target": "@components/control-ui/knob-contracts/tree-knobs.ts",
      "type": "registry:component",
      "content": "// Generated from src/registry/sources/control-ui/recipes/tree.css by scripts/gen-knob-contracts.ts — run `bun run sync:knobs`.\nexport const treeKnobs = [\n  \"--cui-tree-item-trigger-radius\",\n  \"--cui-tree-item-trigger-foreground\",\n  \"--cui-tree-item-trigger-hover-background\",\n  \"--cui-tree-item-trigger-hover-foreground\",\n  \"--cui-tree-item-trigger-selected-background\",\n  \"--cui-tree-item-trigger-selected-foreground\",\n  \"--cui-tree-item-trigger-font-size\",\n  \"--cui-tree-item-trigger-selected-font-weight\",\n] as const;\nexport type TreeKnobStyle = Partial<Record<(typeof treeKnobs)[number], string>>;\n"
    },
    {
      "path": "src/registry/sources/control-ui/recipes/tree.css",
      "target": "@components/control-ui/styles/recipes/tree.css",
      "type": "registry:file",
      "content": "@layer components {\n  :where([data-control-family=\"tree\"][data-slot=\"root\"]) {\n    --cui-tree-item-trigger-radius: var(--radius-popup-item);\n    --cui-tree-item-trigger-foreground: var(--foreground);\n    --cui-tree-item-trigger-hover-background: oklch(from var(--foreground) l c h / 0.04);\n    --cui-tree-item-trigger-hover-foreground: var(--foreground);\n    --cui-tree-item-trigger-selected-background: oklch(from var(--foreground) l c h / 0.06);\n    --cui-tree-item-trigger-selected-foreground: var(--foreground);\n    --cui-tree-item-trigger-font-size: var(--text-body);\n    --cui-tree-item-trigger-selected-font-weight: 400;\n  }\n\n  :where([data-control-family=\"tree\"][data-slot=\"root\"]),\n  :where([data-control-family=\"tree\"][data-slot=\"item\"]) {\n    outline: none;\n  }\n\n  :where([data-control-family=\"tree\"][data-slot=\"item-trigger\"]) {\n    border-radius: var(--cui-tree-item-trigger-radius);\n    background-color: transparent;\n    color: var(--cui-tree-item-trigger-foreground);\n    font-size: var(--cui-tree-item-trigger-font-size);\n    font-weight: 400;\n    outline: none;\n  }\n\n  @media (hover: hover) {\n    :where([data-control-family=\"tree\"][data-slot=\"item-trigger\"]:hover) {\n      background-color: var(--cui-tree-item-trigger-hover-background);\n      color: var(--cui-tree-item-trigger-hover-foreground);\n    }\n  }\n\n  :where([data-control-family=\"tree\"][data-slot=\"item-trigger\"][data-selected]) {\n    background-color: var(--cui-tree-item-trigger-selected-background);\n    color: var(--cui-tree-item-trigger-selected-foreground);\n    font-weight: var(--cui-tree-item-trigger-selected-font-weight);\n  }\n\n  :where([data-control-family=\"tree\"][data-slot=\"item-trigger\"][aria-disabled=\"true\"]) {\n    opacity: 0.5;\n  }\n\n  :where(\n    [data-control-family=\"tree\"][data-indicator=\"slide\"] [data-control-family=\"tree\"][data-slot=\"item-trigger\"]:is(:hover, [data-selected])\n  ) {\n    background-color: transparent;\n  }\n\n  :where([data-control-family=\"tree\"][data-slot=\"item-indicator\"]) {\n    color: var(--muted-foreground);\n  }\n\n  :where([data-control-family=\"tree\"][data-slot=\"item-indicator\"][data-state]) {\n    transition: rotate var(--duration-base) var(--ease-standard);\n  }\n\n  :where([data-control-family=\"tree\"][data-slot=\"item-indicator\"][data-state=\"open\"]) {\n    rotate: 90deg;\n  }\n\n  :where([data-control-family=\"tree\"][data-slot=\"item-content\"]) {\n    height: var(--collapsible-panel-height);\n    overflow: hidden;\n    opacity: 1;\n    transition-property: height, opacity;\n    transition-duration: var(--duration-base);\n    transition-timing-function: var(--ease-emphasized);\n  }\n\n  :where([data-control-family=\"tree\"][data-slot=\"item-content\"]:is([data-starting-style], [data-ending-style])) {\n    height: 0;\n    opacity: 0;\n  }\n\n  :where([data-control-family=\"tree\"][data-slot=\"item-group\"]) {\n    translate: 0 0;\n    transition: translate var(--duration-base) var(--ease-emphasized);\n  }\n\n  :where([data-control-family=\"tree\"][data-slot=\"item-content\"]:is([data-starting-style], [data-ending-style]) > [data-slot=\"item-group\"]) {\n    translate: 0 -0.25rem;\n  }\n}\n\n@property --cui-tree-item-trigger-radius {\n  syntax: \"<length-percentage>\";\n  inherits: true;\n  initial-value: 0px;\n}\n\n@property --cui-tree-item-trigger-foreground {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n\n@property --cui-tree-item-trigger-hover-background {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n\n@property --cui-tree-item-trigger-hover-foreground {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n\n@property --cui-tree-item-trigger-selected-background {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n\n@property --cui-tree-item-trigger-selected-foreground {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n\n@property --cui-tree-item-trigger-font-size {\n  syntax: \"<length-percentage>\";\n  inherits: true;\n  initial-value: 1rem;\n}\n\n@property --cui-tree-item-trigger-selected-font-weight {\n  syntax: \"<number>\";\n  inherits: true;\n  initial-value: 400;\n}\n"
    },
    {
      "path": "src/registry/sources/control-ui/ui/tree.tsx",
      "target": "@components/control-ui/ui/tree.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { Collapsible as CollapsiblePrimitive } from \"@base-ui/react/collapsible\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport { ChevronRightIcon } from \"lucide-react\";\nimport type { ComponentProps, CSSProperties, KeyboardEvent, MouseEvent, ReactNode, Ref } from \"react\";\nimport { Children, createContext, isValidElement, lazy, Suspense, useContext, useRef, useState } from \"react\";\nimport type { RenderProp, SelectionIndicator } from \"@/components/control-ui/control-props\";\nimport type { TreeKnobStyle } from \"@/components/control-ui/knob-contracts/tree-knobs\";\nimport { cn } from \"@/components/control-ui/lib/cn\";\nimport { skinIndicator } from \"@/components/control-ui/skin\";\n\nexport type TreeSelectionMode = \"none\" | \"single\" | \"multiple\";\n\nexport type TreeInteractionReason = \"pointer\" | \"keyboard\" | \"imperative\";\n\nexport type TreeSelectionChangeDetails = {\n  value: string;\n  reason: TreeInteractionReason;\n};\n\nexport type TreeExpandedChangeDetails = {\n  value: string;\n  expanded: boolean;\n  reason: TreeInteractionReason;\n};\n\nexport type TreeSelectionIndicator = SelectionIndicator;\n\nexport type TreeProps = Omit<Omit<ComponentProps<\"ul\">, \"onChange\" | \"defaultValue\">, \"style\"> & {\n  style?: CSSProperties & TreeKnobStyle;\n} & {\n  selectionMode?: TreeSelectionMode;\n  value?: string[];\n  defaultValue?: string[];\n  onValueChange?: (value: string[], details: TreeSelectionChangeDetails) => void;\n  expandedValue?: string[];\n  defaultExpandedValue?: string[];\n  onExpandedChange?: (expanded: string[], details: TreeExpandedChangeDetails) => void;\n  indicator?: TreeSelectionIndicator;\n};\n\nexport type TreeItemProps = Omit<ComponentProps<\"li\">, \"value\"> & {\n  value: string;\n  disabled?: boolean;\n  label?: string;\n  children?: ReactNode;\n};\n\nexport type TreeItemTriggerProps = Omit<ComponentProps<\"div\">, \"style\"> & { style?: CSSProperties & TreeKnobStyle } & {\n  render?: RenderProp<ComponentProps<\"div\">>;\n};\n\nexport type TreeItemIndicatorProps = Omit<ComponentProps<\"span\">, \"style\"> & { style?: CSSProperties & TreeKnobStyle };\n\nexport type TreeItemLabelProps = ComponentProps<\"span\"> & {\n  render?: RenderProp<ComponentProps<\"span\">>;\n};\n\nexport type TreeItemContentProps = ComponentProps<\"div\"> & { style?: CSSProperties & TreeKnobStyle };\n\n// lazy because highlight drags in JS geometry engine; it is decorative, so null fallback is fine\nconst TrackHighlight = lazy(() =>\n  import(\"@/components/control-ui/extensions/track-highlight\").then((module) => ({ default: module.TrackHighlight })),\n);\n\n/*\n * Library-original: Base UI ships no Tree. Follows WAI-ARIA APG treeview — <li> is focusable treeitem, trigger row presentational.\n * Keyboard nav derives visible order from registered item refs at keydown time; collapsed panels unmount, so registry holds exactly reachable items.\n */\n\nconst TYPEAHEAD_TIMEOUT = 500;\n\ntype TreeContextValue = {\n  selectionMode: TreeSelectionMode;\n  indicator: TreeSelectionIndicator;\n  selected: ReadonlySet<string>;\n  expanded: ReadonlySet<string>;\n  focusedValue: string | null;\n  registerItem: (item: RegisteredTreeItem) => () => void;\n  registerLabel: (value: string, node: HTMLSpanElement | null) => (() => void) | undefined;\n  getVisibleItems: () => RegisteredTreeItem[];\n  getItemFromTarget: (target: HTMLElement, root: HTMLElement) => RegisteredTreeItem | undefined;\n  getLabel: (value: string) => string;\n  getItem: (value: string) => RegisteredTreeItem | undefined;\n  select: (value: string, reason: \"pointer\" | \"keyboard\", toggle: boolean) => void;\n  toggleExpanded: (value: string, force?: boolean, reason?: \"pointer\" | \"keyboard\") => void;\n  setFocusedValue: (value: string) => void;\n};\n\nconst TreeContext = createContext<TreeContextValue | null>(null);\n\nfunction useTree() {\n  const context = useContext(TreeContext);\n  if (!context) {\n    throw new Error(\"Tree parts must be used within <Tree>.\");\n  }\n  return context;\n}\n\ntype TreeItemContextValue = {\n  value: string;\n  level: number;\n  disabled: boolean;\n  expandable: boolean;\n};\n\nconst TreeItemContext = createContext<TreeItemContextValue | null>(null);\n\nfunction useTreeItem() {\n  const context = useContext(TreeItemContext);\n  if (!context) {\n    throw new Error(\"TreeItem parts must be used within <TreeItem>.\");\n  }\n  return context;\n}\n\ntype TreeItemStyle = CSSProperties & { \"--_tree-level\"?: number };\ntype TypeaheadRef = { current: { query: string; at: number } };\ntype RegisteredTreeItem = {\n  value: string;\n  node: HTMLLIElement;\n  level: number;\n  disabled: boolean;\n  expandable: boolean;\n  label?: string;\n};\n\nfunction setRef<T>(ref: Ref<T> | undefined, value: T | null) {\n  if (typeof ref === \"function\") return ref(value);\n  if (ref) ref.current = value;\n}\n\nfunction labelOf(item: RegisteredTreeItem, ctx: TreeContextValue): string {\n  return item.label ?? ctx.getLabel(item.value);\n}\n\nfunction compareTreeItems(a: RegisteredTreeItem, b: RegisteredTreeItem): number {\n  const position = a.node.compareDocumentPosition(b.node);\n  if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1;\n  if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1;\n  return 0;\n}\n\nfunction visibleItems(items: ReadonlyMap<string, RegisteredTreeItem>): RegisteredTreeItem[] {\n  return [...items.values()].filter((item) => !item.disabled && item.node.checkVisibility()).sort(compareTreeItems);\n}\n\nfunction focusItem(item: RegisteredTreeItem | undefined, ctx: TreeContextValue): void {\n  if (!item) return;\n  ctx.setFocusedValue(item.value);\n  item.node.focus();\n}\n\nfunction focusParentItem(items: RegisteredTreeItem[], index: number, level: number, ctx: TreeContextValue): void {\n  for (let itemIndex = index - 1; itemIndex >= 0; itemIndex--) {\n    const item = items[itemIndex];\n    if (!item || item.level !== level - 1) continue;\n    focusItem(item, ctx);\n    return;\n  }\n}\n\nfunction handleTypeahead(\n  event: KeyboardEvent<HTMLUListElement>,\n  items: RegisteredTreeItem[],\n  index: number,\n  ctx: TreeContextValue,\n  typeahead: TypeaheadRef,\n): void {\n  if (event.key.length !== 1 || event.metaKey || event.ctrlKey || event.altKey) return;\n  const now = Date.now();\n  typeahead.current.query = now - typeahead.current.at > TYPEAHEAD_TIMEOUT ? event.key : typeahead.current.query + event.key;\n  typeahead.current.at = now;\n  const query = typeahead.current.query.toLowerCase();\n  const ordered = [...items.slice(index + 1), ...items.slice(0, index + 1)];\n  const match = ordered.find((item) => labelOf(item, ctx).toLowerCase().startsWith(query));\n  if (match) focusItem(match, ctx);\n}\n\ntype TreeKeyboardState = {\n  value: string;\n  items: RegisteredTreeItem[];\n  index: number;\n  isBranch: boolean;\n  isExpanded: boolean;\n  level: number;\n};\n\nfunction registeredItemFromTarget(\n  target: HTMLElement,\n  root: HTMLElement,\n  itemsByNode: WeakMap<HTMLElement, RegisteredTreeItem>,\n): RegisteredTreeItem | undefined {\n  let node: HTMLElement | null = target;\n  while (node && root.contains(node)) {\n    const item = itemsByNode.get(node);\n    if (item) return item;\n    if (node === root) return;\n    node = node.parentElement;\n  }\n}\n\nfunction getTreeKeyboardState(event: KeyboardEvent<HTMLUListElement>, ctx: TreeContextValue): TreeKeyboardState | null {\n  if (event.defaultPrevented || !(event.target instanceof HTMLElement)) return null;\n  const current = ctx.getItemFromTarget(event.target, event.currentTarget);\n  if (!current) return null;\n\n  const items = ctx.getVisibleItems();\n  const index = items.indexOf(current);\n  if (index === -1) return null;\n\n  return {\n    value: current.value,\n    items,\n    index,\n    isBranch: current.expandable,\n    isExpanded: ctx.expanded.has(current.value),\n    level: current.level,\n  };\n}\n\nfunction handleTreeKeyDown(event: KeyboardEvent<HTMLUListElement>, ctx: TreeContextValue, typeahead: TypeaheadRef): void {\n  const state = getTreeKeyboardState(event, ctx);\n  if (!state) return;\n  const { value, items, index, isBranch, isExpanded, level } = state;\n\n  switch (event.key) {\n    case \"ArrowDown\":\n      event.preventDefault();\n      focusItem(items[index + 1], ctx);\n      break;\n    case \"ArrowUp\":\n      event.preventDefault();\n      focusItem(items[index - 1], ctx);\n      break;\n    case \"ArrowRight\":\n      event.preventDefault();\n      if (isBranch && !isExpanded) {\n        ctx.toggleExpanded(value, true, \"keyboard\");\n      } else if (isBranch && isExpanded) {\n        focusItem(items[index + 1], ctx);\n      }\n      break;\n    case \"ArrowLeft\":\n      event.preventDefault();\n      if (isBranch && isExpanded) {\n        ctx.toggleExpanded(value, false, \"keyboard\");\n      } else {\n        focusParentItem(items, index, level, ctx);\n      }\n      break;\n    case \"Home\":\n      event.preventDefault();\n      focusItem(items[0], ctx);\n      break;\n    case \"End\":\n      event.preventDefault();\n      focusItem(items[items.length - 1], ctx);\n      break;\n    case \"Enter\":\n      event.preventDefault();\n      ctx.select(value, \"keyboard\", false);\n      if (isBranch) ctx.toggleExpanded(value, undefined, \"keyboard\");\n      break;\n    case \" \":\n      event.preventDefault();\n      ctx.select(value, \"keyboard\", ctx.selectionMode === \"multiple\");\n      break;\n    default:\n      handleTypeahead(event, items, index, ctx, typeahead);\n  }\n}\n\nexport function Tree({\n  selectionMode = \"single\",\n  indicator,\n  value,\n  defaultValue,\n  onValueChange,\n  expandedValue,\n  defaultExpandedValue,\n  onExpandedChange,\n  className,\n  style,\n  children,\n  onKeyDown,\n  onFocusCapture,\n  ...props\n}: TreeProps) {\n  const [selectedState, setSelectedState] = useState(() => new Set(defaultValue));\n  const [expandedState, setExpandedState] = useState(() => new Set(defaultExpandedValue));\n  const [focusedValue, setFocusedValue] = useState<string | null>(null);\n\n  const selected = value ? new Set(value) : selectedState;\n  const expanded = expandedValue ? new Set(expandedValue) : expandedState;\n\n  const typeahead = useRef({ query: \"\", at: 0 });\n  const itemsRef = useRef(new Map<string, RegisteredTreeItem>());\n  const itemsByNodeRef = useRef(new WeakMap<HTMLElement, RegisteredTreeItem>());\n  const labelsRef = useRef(new Map<string, HTMLSpanElement>());\n  const seededFocusRef = useRef(false);\n  const registerItem = (item: RegisteredTreeItem) => {\n    const previous = itemsRef.current.get(item.value);\n    if (previous) itemsByNodeRef.current.delete(previous.node);\n    itemsRef.current.set(item.value, item);\n    itemsByNodeRef.current.set(item.node, item);\n    if (!seededFocusRef.current && !item.disabled) {\n      seededFocusRef.current = true;\n      setFocusedValue(item.value);\n    }\n    return () => {\n      if (itemsRef.current.get(item.value) !== item) return;\n      itemsRef.current.delete(item.value);\n      itemsByNodeRef.current.delete(item.node);\n    };\n  };\n  const registerLabel = (itemValue: string, node: HTMLSpanElement | null) => {\n    if (!node) return;\n    labelsRef.current.set(itemValue, node);\n    return () => {\n      if (labelsRef.current.get(itemValue) === node) labelsRef.current.delete(itemValue);\n    };\n  };\n  const getVisibleItems = () => visibleItems(itemsRef.current);\n  const getItemFromTarget = (target: HTMLElement, root: HTMLElement) => registeredItemFromTarget(target, root, itemsByNodeRef.current);\n  const getLabel = (itemValue: string) => labelsRef.current.get(itemValue)?.textContent ?? \"\";\n  const getItem = (itemValue: string) => itemsRef.current.get(itemValue);\n\n  const select = (itemValue: string, reason: \"pointer\" | \"keyboard\", toggle: boolean) => {\n    if (selectionMode === \"none\") return;\n    let next: Set<string>;\n    if (selectionMode === \"single\" || !toggle) {\n      next = new Set([itemValue]);\n    } else {\n      next = new Set(selected);\n      if (next.has(itemValue)) next.delete(itemValue);\n      else next.add(itemValue);\n    }\n    if (!value) setSelectedState(next);\n    onValueChange?.([...next], { value: itemValue, reason });\n  };\n\n  const toggleExpanded = (itemValue: string, force?: boolean, reason: \"pointer\" | \"keyboard\" = \"pointer\") => {\n    const willExpand = force ?? !expanded.has(itemValue);\n    const next = new Set(expanded);\n    if (willExpand) next.add(itemValue);\n    else next.delete(itemValue);\n    if (!expandedValue) setExpandedState(next);\n    onExpandedChange?.([...next], { value: itemValue, expanded: willExpand, reason });\n  };\n\n  // prop → skin → off\n  const resolvedIndicator = indicator ?? skinIndicator(\"tree\") ?? \"none\";\n\n  const contextValue: TreeContextValue = {\n    selectionMode,\n    indicator: resolvedIndicator,\n    selected,\n    expanded,\n    focusedValue,\n    registerItem,\n    registerLabel,\n    getVisibleItems,\n    getItemFromTarget,\n    getLabel,\n    getItem,\n    select,\n    toggleExpanded,\n    setFocusedValue,\n  };\n  const rootStyle = style;\n  const sliding = resolvedIndicator === \"slide\";\n\n  const list = (\n    <ul\n      data-control-ui=\"tree\"\n      data-control-family=\"tree\"\n      data-indicator={resolvedIndicator}\n      data-slot=\"root\"\n      // biome-ignore lint/a11y/noNoninteractiveElementToInteractiveRole: WAI-ARIA treeview requires role=\"tree\" on the <ul> container.\n      role=\"tree\"\n      aria-multiselectable={selectionMode === \"multiple\" || undefined}\n      className={cn(\"flex flex-col\", sliding ? undefined : className)}\n      style={sliding ? undefined : rootStyle}\n      onKeyDown={(event) => {\n        onKeyDown?.(event);\n        handleTreeKeyDown(event, contextValue, typeahead);\n      }}\n      onFocusCapture={(event) => {\n        onFocusCapture?.(event);\n        if (event.target instanceof HTMLElement) {\n          const focused = contextValue.getItemFromTarget(event.target, event.currentTarget);\n          if (focused) setFocusedValue(focused.value);\n        }\n      }}\n      {...props}\n    >\n      {children}\n    </ul>\n  );\n\n  return (\n    <TreeContext.Provider value={contextValue}>\n      {sliding ? (\n        // wrapped, never injected into list: pill must stay sibling of treeitems for the <ul> content model to hold\n        <div\n          data-control-ui=\"tree\"\n          data-control-family=\"tree\"\n          data-slot=\"track\"\n          data-indicator={resolvedIndicator}\n          className={cn(\"relative isolate\", className)}\n          style={rootStyle}\n        >\n          <Suspense fallback={null}>\n            <TrackHighlight\n              itemSelector=\"[data-control-ui=tree][data-slot=item-trigger]\"\n              activeSelector=\"[data-control-ui=tree][data-slot=item-trigger][data-selected]\"\n            />\n          </Suspense>\n          {list}\n        </div>\n      ) : (\n        list\n      )}\n    </TreeContext.Provider>\n  );\n}\n\nexport function TreeItem({ value, disabled = false, label, className, style, children, ref, ...props }: TreeItemProps) {\n  const tree = useTree();\n  const parent = useContext(TreeItemContext);\n  const level = (parent?.level ?? 0) + 1;\n  const expandable = Children.toArray(children).some((child) => isValidElement(child) && child.type === TreeItemContent);\n\n  const expanded = expandable && tree.expanded.has(value);\n  const selected = tree.selected.has(value);\n  const itemContext: TreeItemContextValue = { value, level, disabled, expandable };\n  const itemRef = (node: HTMLElement | null) => {\n    const itemNode = node instanceof HTMLLIElement ? node : null;\n    const refCleanup = setRef(ref, itemNode);\n    if (!itemNode) return;\n    const unregister = tree.registerItem({ value, node: itemNode, level, disabled, expandable, label });\n    return () => {\n      unregister();\n      if (typeof refCleanup === \"function\") refCleanup();\n      else setRef(ref, null);\n    };\n  };\n\n  const baseClass = cn(\"list-none\", className);\n  const itemStyle = { \"--_tree-level\": level, ...style } satisfies TreeItemStyle;\n\n  const shared = {\n    ...props,\n    \"data-control-ui\": \"tree\",\n    \"data-control-family\": \"tree\",\n    \"data-slot\": \"item\",\n    \"data-value\": value,\n    \"data-label\": label,\n    \"data-selected\": selected || undefined,\n    \"data-disabled\": disabled || undefined,\n    role: \"treeitem\",\n    \"aria-level\": level,\n    \"aria-selected\": tree.selectionMode === \"none\" ? undefined : selected,\n    \"aria-disabled\": disabled || undefined,\n    tabIndex: tree.focusedValue === value ? 0 : -1,\n    style: itemStyle,\n  };\n\n  if (!expandable) {\n    return (\n      <TreeItemContext.Provider value={itemContext}>\n        <li ref={itemRef} {...shared} className={baseClass}>\n          {children}\n        </li>\n      </TreeItemContext.Provider>\n    );\n  }\n\n  return (\n    <TreeItemContext.Provider value={itemContext}>\n      <CollapsiblePrimitive.Root\n        ref={itemRef}\n        open={expanded}\n        onOpenChange={(open) => tree.toggleExpanded(value, open)}\n        render={(renderProps) => (\n          <li\n            {...renderProps}\n            {...shared}\n            role=\"treeitem\"\n            aria-expanded={expanded}\n            tabIndex={tree.focusedValue === value ? 0 : -1}\n            data-state={expanded ? \"open\" : \"closed\"}\n            className={cn(renderProps.className, baseClass)}\n          />\n        )}\n      >\n        {children}\n      </CollapsiblePrimitive.Root>\n    </TreeItemContext.Provider>\n  );\n}\n\n// presentational — the <li> above owns focus\n\nexport function TreeItemTrigger({ className, children, onClick, render, ...props }: TreeItemTriggerProps) {\n  const tree = useTree();\n  const item = useTreeItem();\n  const expanded = item.expandable && tree.expanded.has(item.value);\n  const selected = tree.selected.has(item.value);\n\n  return useRender({\n    defaultTagName: \"div\",\n    render,\n    props: {\n      ...props,\n      \"data-control-ui\": \"tree\",\n      \"data-control-family\": \"tree\",\n      \"data-slot\": \"item-trigger\",\n      \"data-state\": expanded ? \"open\" : \"closed\",\n      \"data-selected\": selected || undefined,\n      \"aria-disabled\": item.disabled || undefined,\n      onClick: (event: MouseEvent<HTMLDivElement>) => {\n        onClick?.(event);\n        if (item.disabled) return;\n        const toggle = tree.selectionMode === \"multiple\" && (event.metaKey || event.ctrlKey);\n        tree.select(item.value, \"pointer\", toggle);\n        if (item.expandable) tree.toggleExpanded(item.value);\n        focusItem(tree.getItem(item.value), tree);\n      },\n      className: cn(\n        \"flex cursor-pointer select-none items-center gap-1.5 py-1 pr-2\",\n        \"pl-[calc(var(--\\\\_tree-level,1)*1.25rem)]\",\n        \"aria-disabled:pointer-events-none\",\n        className,\n      ),\n      children,\n    },\n  });\n}\n\nexport function TreeItemIndicator({ className, children, ...props }: TreeItemIndicatorProps) {\n  const tree = useTree();\n  const item = useTreeItem();\n\n  if (!item.expandable) {\n    return (\n      <span\n        data-control-ui=\"tree\"\n        data-control-family=\"tree\"\n        data-slot=\"item-indicator\"\n        aria-hidden\n        className={cn(\"inline-flex size-4 shrink-0\", className)}\n        {...props}\n      />\n    );\n  }\n\n  // rotates off its OWN state: nested chevron sits inside parent's open <li>, so descendant selector would rotate every child when ancestor opens\n  const expanded = tree.expanded.has(item.value);\n\n  return (\n    <span\n      data-control-ui=\"tree\"\n      data-control-family=\"tree\"\n      data-slot=\"item-indicator\"\n      data-state={expanded ? \"open\" : \"closed\"}\n      aria-hidden\n      className={cn(\"inline-flex size-4 shrink-0 items-center justify-center\", className)}\n      {...props}\n    >\n      {children ?? <ChevronRightIcon className=\"size-4\" />}\n    </span>\n  );\n}\n\n// accessible name and type-ahead target\n\nexport function TreeItemLabel({ className, children, render, ref, ...props }: TreeItemLabelProps) {\n  const tree = useTree();\n  const item = useTreeItem();\n  const labelRef = (node: HTMLSpanElement | null) => {\n    const unregister = tree.registerLabel(item.value, node);\n    const cleanup = setRef(ref, node);\n    if (!node) return;\n    return () => {\n      unregister?.();\n      if (typeof cleanup === \"function\") cleanup();\n      else setRef(ref, null);\n    };\n  };\n\n  return useRender({\n    defaultTagName: \"span\",\n    render,\n    props: {\n      ...props,\n      ref: labelRef,\n      \"data-control-ui\": \"tree\",\n      \"data-control-family\": \"tree\",\n      \"data-slot\": \"item-label\",\n      className: cn(\"min-w-0 flex-1 truncate\", className),\n      children,\n    },\n  });\n}\n\n// Enter and exit both ride data-starting-style/data-ending-style, which Base UI sets only during transition, so slide plays on close too and never sticks.\nexport function TreeItemContent({ className, children, ...props }: TreeItemContentProps) {\n  return (\n    <CollapsiblePrimitive.Panel\n      {...props}\n      render={(renderProps, state) => (\n        <div\n          {...renderProps}\n          data-control-ui=\"tree\"\n          data-control-family=\"tree\"\n          data-slot=\"item-content\"\n          data-state={state.open ? \"open\" : \"closed\"}\n          className={renderProps.className}\n        />\n      )}\n    >\n      {/* biome-ignore lint/a11y/useSemanticElements: WAI-ARIA treeview requires role=\"group\" on the nested child list. */}\n      <ul role=\"group\" data-control-ui=\"tree\" data-control-family=\"tree\" data-slot=\"item-group\" className={cn(\"flex flex-col\", className)}>\n        {children}\n      </ul>\n    </CollapsiblePrimitive.Panel>\n  );\n}\n"
    }
  ],
  "css": {
    "@import \"../components/control-ui/styles/recipes/tree.css\"": {}
  },
  "meta": {}
}
