{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tree",
  "title": "Tree",
  "description": "A tree component.",
  "dependencies": [
    "@hugeicons/react",
    "@hugeicons/core-free-icons"
  ],
  "registryDependencies": [
    "@cubby-ui/checkbox",
    "@cubby-ui/elevated"
  ],
  "files": [
    {
      "path": "registry/default/tree/tree.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Collapsible as BaseCollapsible } from \"@base-ui/react/collapsible\";\nimport { CheckboxGroup } from \"@base-ui/react/checkbox-group\";\nimport { Checkbox } from \"@/registry/default/checkbox/checkbox\";\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport { ChevronRightIcon, Loading03Icon } from \"@hugeicons/core-free-icons\";\nimport { cn } from \"@/lib/utils\";\nimport { solidSurface } from \"@/registry/default/lib/elevated\";\nimport {\n  getAllDescendantIds,\n  getLeafNodeIds,\n  buildParentMap,\n  collectVisibleIds,\n  handleTreeKeyboardNavigation,\n} from \"./lib/tree-utils\";\nimport * as TreeUtils from \"./lib/tree-utils\";\n\nexport interface TreeNodeBase<\n  TData extends Record<string, unknown> = Record<string, unknown>,\n> {\n  id: string;\n  name: string;\n  data?: TData;\n  disabled?: boolean;\n  loading?: boolean;\n  icon?: React.ReactNode;\n  badge?: React.ReactNode;\n}\n\nexport interface TreeLeafNode<\n  TData extends Record<string, unknown> = Record<string, unknown>,\n> extends TreeNodeBase<TData> {\n  children?: never;\n}\n\nexport interface TreeParentNode<\n  TData extends Record<string, unknown> = Record<string, unknown>,\n> extends TreeNodeBase<TData> {\n  children: TreeNode<TData>[];\n  iconOpen?: React.ReactNode;\n  onLoadChildren?: () => Promise<TreeNode<TData>[]>;\n}\n\nexport type TreeNode<\n  TData extends Record<string, unknown> = Record<string, unknown>,\n> = TreeLeafNode<TData> | TreeParentNode<TData>;\n\nexport interface TreeSelectEvent<\n  TData extends Record<string, unknown> = Record<string, unknown>,\n> {\n  nodeId: string;\n  node: TreeNode<TData>;\n  event: React.MouseEvent | React.KeyboardEvent;\n}\n\nexport interface TreeExpandEvent {\n  nodeId: string;\n  expanded: boolean;\n}\n\ntype TreeVariant = \"default\" | \"filled\" | \"outline\";\ntype TreeSize = \"default\" | \"sm\";\ntype TreeMode = \"single\" | \"multiple\" | \"none\";\n\ninterface TreeContextValue<\n  TData extends Record<string, unknown> = Record<string, unknown>,\n> {\n  // Tree-level state\n  selectedNode?: string | null;\n  onNodeSelect?: (nodeId: string) => void;\n  expandedNodes: Set<string>;\n  onToggleNode: (nodeId: string) => void | Promise<void>;\n  checkedNodes?: Set<string>;\n  onCheckedNodesChange?: (checked: string[]) => void;\n  renderItem: (item: TreeNode<TData>) => React.ReactElement;\n  variant: TreeVariant;\n  size: TreeSize;\n  /** Some node has children, so leaves keep the chevron's room. */\n  hasParents: boolean;\n  showLines: boolean;\n  enableBulkActions: boolean;\n  disableSelection: boolean;\n  focusableNodes: React.MutableRefObject<Map<string, HTMLElement>>;\n  visibleNodeIds: string[];\n  disabledNodesMap: Map<string, boolean>;\n  loadingNodesMap: Map<string, boolean>;\n  parentNodeMap: Map<string, string>;\n  lastFocusedNodeId: string | null;\n  setLastFocusedNodeId: (nodeId: string | null) => void;\n  data: TreeNode<TData>[];\n  // Item-level state\n  item?: TreeNode<TData>;\n  depth?: number;\n  isExpanded?: boolean;\n  isSelected?: boolean;\n  hasChildren?: boolean;\n  isLoading?: boolean;\n}\n\nconst TreeContext = React.createContext<TreeContextValue | null>(null);\n\nfunction useTreeContext<\n  TData extends Record<string, unknown> = Record<string, unknown>,\n>(): TreeContextValue<TData> {\n  const context = React.useContext(\n    TreeContext,\n  ) as TreeContextValue<TData> | null;\n  if (!context) {\n    throw new Error(\"Tree components must be used within a Tree\");\n  }\n  return context;\n}\n\n// Extracts item-level state from context for TreeItem subcomponents\nfunction useTreeItemContext<\n  TData extends Record<string, unknown> = Record<string, unknown>,\n>(): Required<\n  Pick<\n    TreeContextValue<TData>,\n    \"item\" | \"depth\" | \"isExpanded\" | \"isSelected\" | \"hasChildren\" | \"isLoading\"\n  >\n> {\n  const context = useTreeContext<TData>();\n  if (!context.item) {\n    throw new Error(\"TreeItem subcomponents must be used within TreeItem\");\n  }\n  return {\n    item: context.item,\n    depth: context.depth!,\n    isExpanded: context.isExpanded!,\n    isSelected: context.isSelected!,\n    hasChildren: context.hasChildren!,\n    isLoading: context.isLoading!,\n  };\n}\n\nexport interface TreeProps<\n  TData extends Record<string, unknown> = Record<string, unknown>,\n> extends Omit<useRender.ComponentProps<\"div\">, \"children\"> {\n  data: TreeNode<TData>[];\n  children: (item: TreeNode<TData>) => React.ReactElement;\n  selectedNode?: string | null;\n  onNodeSelect?: (nodeId: string) => void;\n  defaultExpanded?: string[];\n  expanded?: string[];\n  onExpandedChange?: (expanded: string[]) => void;\n  checkedNodes?: string[];\n  onCheckedNodesChange?: (checked: string[]) => void;\n  variant?: TreeVariant;\n  /** Row density: `sm` for compact trees such as a file explorer. */\n  size?: TreeSize;\n  showLines?: boolean;\n  mode?: TreeMode;\n}\n\nfunction Tree<TData extends Record<string, unknown> = Record<string, unknown>>({\n  data,\n  children: renderItem,\n  selectedNode,\n  onNodeSelect,\n  defaultExpanded = [],\n  expanded,\n  onExpandedChange,\n  checkedNodes,\n  onCheckedNodesChange,\n  variant = \"default\",\n  size = \"default\",\n  showLines = false,\n  mode = \"single\",\n  className,\n  render,\n  ...props\n}: TreeProps<TData>): React.ReactElement {\n  const enableBulkActions = mode === \"multiple\";\n  const disableSelection = mode === \"none\";\n\n  const renderItemRef = React.useRef(renderItem);\n  renderItemRef.current = renderItem;\n\n  const [internalExpanded, setInternalExpanded] = React.useState<Set<string>>(\n    () => new Set(defaultExpanded),\n  );\n\n  const focusableNodes = React.useRef<Map<string, HTMLElement>>(new Map());\n  const [lastFocusedNodeId, setLastFocusedNodeId] = React.useState<\n    string | null\n  >(null);\n\n  const [loadingNodes, setLoadingNodes] = React.useState<Set<string>>(\n    () => new Set(),\n  );\n\n  const loadedNodesRef = React.useRef<Set<string>>(new Set());\n\n  const [loadedChildren, setLoadedChildren] = React.useState<\n    Map<string, TreeNode<TData>[]>\n  >(() => new Map());\n\n  const expandedNodes = React.useMemo(() => {\n    return expanded ? new Set(expanded) : internalExpanded;\n  }, [expanded, internalExpanded]);\n\n  const mergedData = React.useMemo(() => {\n    if (loadedChildren.size === 0) return data;\n\n    const mergeChildren = (nodes: TreeNode<TData>[]): TreeNode<TData>[] => {\n      return nodes.map((node) => {\n        const loaded = loadedChildren.get(node.id);\n        if (loaded) {\n          return { ...node, children: loaded };\n        }\n        if (node.children) {\n          return { ...node, children: mergeChildren(node.children) };\n        }\n        return node;\n      });\n    };\n\n    return mergeChildren(data);\n  }, [data, loadedChildren]);\n\n  const handleToggleNode = React.useCallback(\n    async (nodeId: string) => {\n      const isCurrentlyExpanded = expandedNodes.has(nodeId);\n      const isExpanding = !isCurrentlyExpanded;\n\n      const findNode = (\n        nodes: TreeNode<TData>[],\n      ): TreeNode<TData> | undefined => {\n        for (const node of nodes) {\n          if (node.id === nodeId) return node;\n          if (node.children) {\n            const found = findNode(node.children);\n            if (found) return found;\n          }\n        }\n        return undefined;\n      };\n\n      const node = findNode(mergedData);\n      const needsToLoad =\n        isExpanding &&\n        node &&\n        \"onLoadChildren\" in node &&\n        node.onLoadChildren &&\n        !loadedNodesRef.current.has(nodeId);\n\n      if (needsToLoad) {\n        setLoadingNodes((prev) => new Set(prev).add(nodeId));\n        loadedNodesRef.current.add(nodeId);\n\n        try {\n          const children = await node.onLoadChildren!();\n          setLoadedChildren((prev) => {\n            const next = new Map(prev);\n            next.set(nodeId, children);\n            return next;\n          });\n\n          const newExpanded = new Set(expandedNodes);\n          newExpanded.add(nodeId);\n          if (expanded && onExpandedChange) {\n            onExpandedChange(Array.from(newExpanded));\n          } else {\n            setInternalExpanded(newExpanded);\n          }\n        } catch (error) {\n          console.error(`Failed to load children for node ${nodeId}:`, error);\n          loadedNodesRef.current.delete(nodeId);\n        } finally {\n          setLoadingNodes((prev) => {\n            const next = new Set(prev);\n            next.delete(nodeId);\n            return next;\n          });\n        }\n        return;\n      }\n\n      const newExpanded = new Set(expandedNodes);\n      if (isCurrentlyExpanded) {\n        newExpanded.delete(nodeId);\n      } else {\n        newExpanded.add(nodeId);\n      }\n\n      if (expanded && onExpandedChange) {\n        onExpandedChange(Array.from(newExpanded));\n      } else {\n        setInternalExpanded(newExpanded);\n      }\n    },\n    [expandedNodes, expanded, onExpandedChange, mergedData],\n  );\n\n  const checkedNodesSet = React.useMemo(() => {\n    return checkedNodes ? new Set(checkedNodes) : undefined;\n  }, [checkedNodes]);\n\n  const parentNodeMap = React.useMemo(\n    () => buildParentMap(mergedData),\n    [mergedData],\n  );\n\n  const { visibleNodeIds, disabledNodesMap } = React.useMemo(\n    () => collectVisibleIds(mergedData, expandedNodes),\n    [mergedData, expandedNodes],\n  );\n\n  // Leaves at any level sit beside folders or under one, so one parent\n  // anywhere means a top-level one.\n  const hasParents = React.useMemo(\n    () =>\n      mergedData.some(\n        (node) =>\n          (node.children && node.children.length > 0) ||\n          (\"onLoadChildren\" in node && Boolean(node.onLoadChildren)),\n      ),\n    [mergedData],\n  );\n\n  const loadingNodesMap = React.useMemo(() => {\n    const map = new Map<string, boolean>();\n    loadingNodes.forEach((id) => map.set(id, true));\n    return map;\n  }, [loadingNodes]);\n\n  React.useEffect(() => {\n    if (!lastFocusedNodeId || visibleNodeIds.includes(lastFocusedNodeId))\n      return;\n\n    const firstId = visibleNodeIds[0];\n    if (firstId) {\n      setLastFocusedNodeId(firstId);\n      setTimeout(() => focusableNodes.current.get(firstId)?.focus(), 0);\n    } else {\n      setLastFocusedNodeId(null);\n    }\n  }, [visibleNodeIds, lastFocusedNodeId]);\n\n  const contextValue = React.useMemo(\n    () => ({\n      selectedNode,\n      onNodeSelect,\n      expandedNodes,\n      onToggleNode: handleToggleNode,\n      checkedNodes: checkedNodesSet,\n      onCheckedNodesChange,\n      renderItem: ((item: TreeNode<Record<string, unknown>>) =>\n        renderItemRef.current(item as TreeNode<TData>)) as (\n        item: TreeNode<Record<string, unknown>>,\n      ) => React.ReactElement,\n      variant,\n      size,\n      hasParents,\n      showLines,\n      enableBulkActions,\n      disableSelection,\n      focusableNodes,\n      visibleNodeIds,\n      disabledNodesMap,\n      loadingNodesMap,\n      parentNodeMap,\n      lastFocusedNodeId,\n      setLastFocusedNodeId,\n      data: mergedData,\n    }),\n    [\n      selectedNode,\n      onNodeSelect,\n      expandedNodes,\n      handleToggleNode,\n      checkedNodesSet,\n      onCheckedNodesChange,\n      variant,\n      size,\n      hasParents,\n      showLines,\n      enableBulkActions,\n      disableSelection,\n      visibleNodeIds,\n      disabledNodesMap,\n      loadingNodesMap,\n      parentNodeMap,\n      lastFocusedNodeId,\n      setLastFocusedNodeId,\n      mergedData,\n    ],\n  );\n\n  const defaultProps = {\n    \"data-slot\": \"tree\",\n    className: cn(\n      \"w-full min-w-0 text-sm\",\n      variant === \"filled\" &&\n        cn(\"rounded-2xl p-2\", solidSurface(3, 2), \"bg-muted\"),\n      variant === \"outline\" &&\n        cn(\"overflow-hidden rounded-2xl p-2\", solidSurface(3, 2)),\n      className,\n    ),\n    role: \"tree\",\n    \"aria-multiselectable\": enableBulkActions,\n    children: (\n      <TreeContext.Provider value={contextValue}>\n        {mergedData.map((node, index) => (\n          <TreeItemInternal\n            key={node.id}\n            node={node}\n            depth={0}\n            positionInSet={index + 1}\n            setSize={mergedData.length}\n          />\n        ))}\n      </TreeContext.Provider>\n    ),\n  };\n\n  const element = useRender({\n    defaultTagName: \"div\",\n    render,\n    props: mergeProps<\"div\">(defaultProps, props),\n  });\n\n  return element;\n}\n\n/**\n * Each level indents by exactly the chevron and its gap, so a child's\n * chevron sits under its parent's icon: the alignment that makes nesting\n * read at a glance. (A shorter step, tried for compact trees, left a child\n * folder looking like its parent's sibling.)\n */\nconst INDENT_SIZE = { default: 14 + 8, sm: 14 + 6 } as const;\nconst INDENT_SIZE_WITH_CHECKBOX = 12;\n/** A row's inline padding and half its 14px chevron: the guide line's x. */\nconst LINE_OFFSET = { default: 8 + 7, sm: 6 + 7 } as const;\nconst BADGE_TEXT_SIZE = \"text-[10px]\";\n\n/**\n * Updates parent nodes based on their children's checked state.\n * A parent is checked if all its leaf descendants are checked.\n */\nfunction updateParentCheckStates<\n  TData extends Record<string, unknown> = Record<string, unknown>,\n>(nodes: TreeNode<TData>[], checkedSet: Set<string>): void {\n  for (const node of nodes) {\n    if (node.children && node.children.length > 0) {\n      updateParentCheckStates(node.children, checkedSet);\n\n      const leafIds = getLeafNodeIds(node.children);\n      const allLeavesChecked = leafIds.every((id) => checkedSet.has(id));\n\n      if (allLeavesChecked) {\n        checkedSet.add(node.id);\n      } else {\n        checkedSet.delete(node.id);\n      }\n    }\n  }\n}\n\ninterface UseTreeCheckboxStateParams<\n  TData extends Record<string, unknown> = Record<string, unknown>,\n> {\n  node: TreeNode<TData>;\n  hasChildren: boolean;\n  context: TreeContextValue<TData>;\n}\n\ninterface UseTreeCheckboxStateReturn {\n  allDescendantIds: string[];\n  localChildValues: string[];\n  handleLocalCheckboxChange: (newValues: string[]) => void;\n}\n\nfunction useTreeCheckboxState<\n  TData extends Record<string, unknown> = Record<string, unknown>,\n>({\n  node,\n  hasChildren,\n  context,\n}: UseTreeCheckboxStateParams<TData>): UseTreeCheckboxStateReturn {\n  const allDescendantIds = React.useMemo(\n    () => (hasChildren ? getAllDescendantIds(node) : []),\n    [hasChildren, node],\n  );\n\n  const localChildValues = React.useMemo(() => {\n    if (!hasChildren || !context.checkedNodes || !context.enableBulkActions)\n      return [];\n    return allDescendantIds.filter((id) => context.checkedNodes!.has(id));\n  }, [\n    hasChildren,\n    context.checkedNodes,\n    allDescendantIds,\n    context.enableBulkActions,\n  ]);\n\n  const handleLocalCheckboxChange = React.useCallback(\n    (newValues: string[]) => {\n      if (!context.onCheckedNodesChange || !context.checkedNodes) return;\n\n      const currentSet = new Set(context.checkedNodes);\n\n      for (const id of allDescendantIds) {\n        currentSet.delete(id);\n      }\n\n      for (const id of newValues) {\n        currentSet.add(id);\n      }\n\n      updateParentCheckStates(context.data, currentSet);\n\n      context.onCheckedNodesChange(Array.from(currentSet));\n    },\n    [context, allDescendantIds],\n  );\n\n  return {\n    allDescendantIds,\n    localChildValues,\n    handleLocalCheckboxChange,\n  };\n}\n\ninterface TreeItemInternalProps<\n  TData extends Record<string, unknown> = Record<string, unknown>,\n> {\n  node: TreeNode<TData>;\n  depth: number;\n  positionInSet?: number;\n  setSize?: number;\n}\n\nfunction TreeItemInternal<\n  TData extends Record<string, unknown> = Record<string, unknown>,\n>({\n  node,\n  depth,\n  positionInSet,\n  setSize,\n}: TreeItemInternalProps<TData>): React.ReactElement {\n  const context = useTreeContext<TData>();\n\n  const hasChildren = Boolean(\n    (node.children && node.children.length > 0) ||\n    (\"onLoadChildren\" in node && node.onLoadChildren),\n  );\n  const isExpanded = context.expandedNodes.has(node.id);\n  const isSelected = context.selectedNode === node.id;\n  const isDisabled = node.disabled || false;\n  const isLoading =\n    context.loadingNodesMap.has(node.id) || node.loading || false;\n  const isChecked =\n    context.enableBulkActions && context.checkedNodes\n      ? context.checkedNodes.has(node.id)\n      : false;\n\n  // Roving tabindex: last focused node is tabbable; first node if none focused yet.\n  const isFirstNode = context.visibleNodeIds[0] === node.id;\n  const isLastFocused = context.lastFocusedNodeId === node.id;\n  const isTabbable =\n    isLastFocused || (!context.lastFocusedNodeId && isFirstNode);\n\n  const mergedContextValue = React.useMemo(\n    () => ({\n      ...context,\n      item: node,\n      depth,\n      isExpanded,\n      isSelected,\n      hasChildren,\n      isLoading,\n    }),\n    [context, node, depth, isExpanded, isSelected, hasChildren, isLoading],\n  );\n\n  const handleToggleChecked = React.useCallback(() => {\n    if (!context.onCheckedNodesChange || !context.checkedNodes) return;\n\n    const currentSet = new Set(context.checkedNodes);\n    const allDescendantIds = hasChildren ? getAllDescendantIds(node) : [];\n\n    if (isChecked) {\n      currentSet.delete(node.id);\n      for (const id of allDescendantIds) {\n        currentSet.delete(id);\n      }\n    } else {\n      currentSet.add(node.id);\n      for (const id of allDescendantIds) {\n        currentSet.add(id);\n      }\n    }\n\n    updateParentCheckStates(context.data, currentSet);\n\n    context.onCheckedNodesChange(Array.from(currentSet));\n  }, [context, isChecked, node, hasChildren]);\n\n  const handleClick = React.useCallback(\n    (e: React.MouseEvent) => {\n      if (isDisabled) return;\n      e.stopPropagation();\n\n      if (context.enableBulkActions) {\n        handleToggleChecked();\n      } else if (!context.disableSelection) {\n        context.onNodeSelect?.(node.id);\n      }\n    },\n    [context, node.id, isDisabled, handleToggleChecked],\n  );\n\n  const handleKeyDown = React.useCallback(\n    (e: React.KeyboardEvent) => {\n      handleTreeKeyboardNavigation(e, {\n        nodeId: node.id,\n        hasChildren,\n        isExpanded,\n        isDisabled,\n        node,\n        visibleNodeIds: context.visibleNodeIds,\n        disabledNodesMap: context.disabledNodesMap,\n        parentNodeMap: context.parentNodeMap,\n        focusableNodes: context.focusableNodes,\n        enableBulkActions: context.enableBulkActions,\n        disableSelection: context.disableSelection,\n        onToggleNode: context.onToggleNode,\n        onNodeSelect: context.onNodeSelect,\n        setLastFocusedNodeId: context.setLastFocusedNodeId,\n        handleToggleChecked,\n      });\n    },\n    [\n      node,\n      hasChildren,\n      isExpanded,\n      isDisabled,\n      context.visibleNodeIds,\n      context.disabledNodesMap,\n      context.parentNodeMap,\n      context.focusableNodes,\n      context.enableBulkActions,\n      context.disableSelection,\n      context.onToggleNode,\n      context.onNodeSelect,\n      context.setLastFocusedNodeId,\n      handleToggleChecked,\n    ],\n  );\n\n  const indent =\n    depth *\n    (context.enableBulkActions\n      ? INDENT_SIZE_WITH_CHECKBOX\n      : INDENT_SIZE[context.size]);\n\n  const { allDescendantIds, localChildValues, handleLocalCheckboxChange } =\n    useTreeCheckboxState({\n      node,\n      hasChildren,\n      context,\n    });\n\n  // The children's group sits beside the row rather than inside it, so the\n  // row names it as its own, once the group is rendered (children that are\n  // still loading have none yet).\n  const groupId = React.useId();\n  const ownsGroup = isExpanded && Boolean(node.children?.length);\n\n  const setFocusable = (el: HTMLElement | null): void => {\n    if (el) {\n      context.focusableNodes.current.set(node.id, el);\n    } else {\n      context.focusableNodes.current.delete(node.id);\n    }\n  };\n\n  const tabIndex = isDisabled ? -1 : isTabbable ? 0 : -1;\n\n  // One element per row: it is the tree item, takes focus and carries hover\n  // and selection (two nested ones, pulled together with negative margins,\n  // used to split the job). Hover lands at once; the focus ring sits inside\n  // the row rather than over its neighbours.\n  const rowClassName = cn(\n    \"flex min-w-0 flex-1 items-center rounded-md border-0 bg-transparent text-left select-none\",\n    context.size === \"sm\"\n      ? \"h-7 gap-1.5 px-1.5 text-[0.8125rem]\"\n      : \"h-8 gap-2 px-2 text-sm\",\n    \"hover:bg-surface-hover\",\n    \"focus-visible:outline-ring/50 outline-0 outline-transparent outline-solid focus-visible:outline-2 focus-visible:-outline-offset-2\",\n    isSelected &&\n      !context.disableSelection &&\n      \"bg-surface-selected text-foreground\",\n    isDisabled ? \"cursor-not-allowed opacity-60\" : \"cursor-pointer\",\n  );\n\n  const itemAria = {\n    role: \"treeitem\",\n    \"aria-level\": depth + 1,\n    \"aria-setsize\": setSize,\n    \"aria-posinset\": positionInSet,\n    \"aria-selected\": context.enableBulkActions ? undefined : isSelected,\n    \"aria-checked\": context.enableBulkActions ? isChecked : undefined,\n    \"aria-disabled\": isDisabled,\n  } as const;\n\n  // Leaves keep the chevron's room, so every label at a level lines up\n  // (they used to start where a folder's chevron does). Outside the row, so\n  // a leaf's highlight starts at its content, not at an empty strip; inside\n  // it only in bulk mode, where the checkboxes lead and must line up too.\n  const leafRoom =\n    !hasChildren && context.hasParents && !context.enableBulkActions\n      ? INDENT_SIZE[context.size]\n      : 0;\n  const chevronSlot = hasChildren ? (\n    <HugeiconsIcon\n      icon={ChevronRightIcon}\n      aria-hidden=\"true\"\n      strokeWidth={2}\n      className={cn(\n        \"text-muted-foreground ease-out-expo size-3.5 shrink-0 transition-[rotate] duration-200\",\n        isExpanded && \"rotate-90\",\n      )}\n    />\n  ) : context.hasParents && context.enableBulkActions ? (\n    <span aria-hidden=\"true\" className=\"size-3.5 shrink-0\" />\n  ) : null;\n\n  if (!hasChildren) {\n    return (\n      <TreeContext.Provider value={mergedContextValue as TreeContextValue}>\n        <div\n          role=\"none\"\n          className=\"mt-0.5 flex first:mt-0\"\n          style={{ paddingLeft: indent + leafRoom }}\n        >\n          <div\n            ref={setFocusable}\n            {...itemAria}\n            tabIndex={tabIndex}\n            className={rowClassName}\n            onClick={handleClick}\n            onKeyDown={(e) => {\n              if (e.key === \" \" || e.key === \"Enter\") {\n                e.stopPropagation();\n              }\n              handleKeyDown(e);\n            }}\n            onFocus={() => {\n              context.setLastFocusedNodeId(node.id);\n            }}\n          >\n            {context.enableBulkActions && (\n              <Checkbox\n                value={node.id}\n                checked={isChecked}\n                disabled={isDisabled}\n                tabIndex={-1}\n                className=\"pointer-events-none\"\n              />\n            )}\n            {chevronSlot}\n            {context.renderItem(node)}\n          </div>\n        </div>\n      </TreeContext.Provider>\n    );\n  }\n\n  const parentContent = (\n    <TreeContext.Provider value={mergedContextValue as TreeContextValue}>\n      <div role=\"none\" className=\"mt-0.5 first:mt-0\">\n        <BaseCollapsible.Root\n          open={isExpanded}\n          onOpenChange={() => !isDisabled && context.onToggleNode(node.id)}\n        >\n          <div role=\"none\" className=\"flex\" style={{ paddingLeft: indent }}>\n            {context.enableBulkActions ? (\n              // Plain div in bulk-actions mode: avoids Collapsible.Trigger's\n              // built-in keyboard handling.\n              <div\n                ref={setFocusable}\n                {...itemAria}\n                aria-expanded={isExpanded}\n                aria-owns={ownsGroup ? groupId : undefined}\n                tabIndex={tabIndex}\n                className={rowClassName}\n                onClick={() => {\n                  if (!isDisabled) {\n                    context.onToggleNode(node.id);\n                  }\n                }}\n                onKeyDown={handleKeyDown}\n                onFocus={() => {\n                  context.setLastFocusedNodeId(node.id);\n                }}\n              >\n                <span\n                  onClick={(e: React.MouseEvent) => {\n                    e.stopPropagation();\n                    handleToggleChecked();\n                  }}\n                >\n                  <Checkbox\n                    parent\n                    disabled={isDisabled}\n                    tabIndex={-1}\n                    className=\"pointer-events-auto cursor-pointer\"\n                  />\n                </span>\n                {chevronSlot}\n                {context.renderItem(node)}\n              </div>\n            ) : (\n              <BaseCollapsible.Trigger\n                ref={setFocusable}\n                {...itemAria}\n                aria-owns={ownsGroup ? groupId : undefined}\n                className={rowClassName}\n                onClick={(e) => {\n                  if (!isDisabled && !context.disableSelection) {\n                    handleClick(e);\n                  }\n                }}\n                onKeyDown={handleKeyDown}\n                onFocus={() => {\n                  context.setLastFocusedNodeId(node.id);\n                }}\n                disabled={isDisabled}\n                tabIndex={tabIndex}\n              >\n                {chevronSlot}\n                {context.renderItem(node)}\n              </BaseCollapsible.Trigger>\n            )}\n          </div>\n\n          <BaseCollapsible.Panel\n            className={cn(\n              \"ease-out-expo h-(--collapsible-panel-height) overflow-y-clip transition-[height,opacity] duration-250\",\n              \"data-ending-style:h-0 data-ending-style:opacity-0\",\n              \"data-starting-style:h-0 data-starting-style:opacity-0\",\n            )}\n          >\n            {node.children && node.children.length > 0 && (\n              <div id={groupId} role=\"group\" className=\"relative pt-0.5\">\n                {/* One guide line per group, under the parent's chevron. */}\n                {context.showLines && (\n                  <div\n                    aria-hidden=\"true\"\n                    className=\"bg-border absolute top-0.5 bottom-0 w-px\"\n                    style={{ left: indent + LINE_OFFSET[context.size] }}\n                  />\n                )}\n                {node.children.map((child, index) => (\n                  <TreeItemInternal\n                    key={child.id}\n                    node={child}\n                    depth={depth + 1}\n                    positionInSet={index + 1}\n                    setSize={node.children?.length}\n                  />\n                ))}\n              </div>\n            )}\n          </BaseCollapsible.Panel>\n        </BaseCollapsible.Root>\n      </div>\n    </TreeContext.Provider>\n  );\n\n  if (context.enableBulkActions) {\n    return (\n      <CheckboxGroup\n        value={localChildValues}\n        onValueChange={handleLocalCheckboxChange}\n        allValues={allDescendantIds}\n      >\n        {parentContent}\n      </CheckboxGroup>\n    );\n  }\n\n  return parentContent;\n}\n\nexport interface TreeItemProps extends useRender.ComponentProps<\"div\"> {\n  children: React.ReactNode;\n}\n\nfunction TreeItem({ className, children, render, ...props }: TreeItemProps) {\n  const defaultProps = {\n    \"data-slot\": \"tree-item\",\n    // min-w-0, or a long label can't shrink below its text to truncate.\n    className: cn(\"flex min-w-0 flex-1 items-center gap-2\", className),\n    children,\n  };\n\n  const element = useRender({\n    defaultTagName: \"div\",\n    render,\n    props: mergeProps<\"div\">(defaultProps, props),\n  });\n\n  return element;\n}\n\nexport interface TreeItemIconProps extends useRender.ComponentProps<\"span\"> {\n  children?: React.ReactNode;\n}\n\nfunction TreeItemIcon({\n  children,\n  className,\n  render,\n  ...props\n}: TreeItemIconProps) {\n  const { item, isExpanded, isLoading } = useTreeItemContext();\n\n  let displayIcon: React.ReactNode;\n  if (isLoading) {\n    displayIcon = (\n      <HugeiconsIcon\n        icon={Loading03Icon}\n        className=\"animate-spin\"\n        strokeWidth={2}\n      />\n    );\n  } else {\n    displayIcon =\n      isExpanded && \"iconOpen\" in item && item.iconOpen\n        ? item.iconOpen\n        : children;\n  }\n\n  const defaultProps = {\n    \"data-slot\": \"tree-item-icon\",\n    className: cn(\"text-muted-foreground shrink-0 [&>svg]:size-4\", className),\n    children: displayIcon,\n  };\n\n  const element = useRender({\n    defaultTagName: \"span\",\n    render,\n    props: mergeProps<\"span\">(defaultProps, props),\n  });\n\n  if (!displayIcon) return null;\n\n  return element;\n}\n\nexport interface TreeItemLabelProps extends useRender.ComponentProps<\"span\"> {\n  children: React.ReactNode;\n}\n\nfunction TreeItemLabel({\n  children,\n  className,\n  render,\n  ...props\n}: TreeItemLabelProps) {\n  const defaultProps = {\n    \"data-slot\": \"tree-item-label\",\n    className: cn(\"truncate\", className),\n    children,\n  };\n\n  const element = useRender({\n    defaultTagName: \"span\",\n    render,\n    props: mergeProps<\"span\">(defaultProps, props),\n  });\n\n  if (!children) return null;\n\n  return element;\n}\n\nexport interface TreeItemBadgeProps extends useRender.ComponentProps<\"span\"> {\n  children?: React.ReactNode;\n}\n\nfunction TreeItemBadge({\n  children,\n  className,\n  render,\n  ...props\n}: TreeItemBadgeProps) {\n  const defaultProps = {\n    \"data-slot\": \"tree-item-badge\",\n    className: cn(\n      \"ml-auto flex shrink-0 items-center\",\n      // Shrink badge text/padding to tree row height without reflowing it.\n      `[&_[data-slot=badge]]:py-0.5 [&_[data-slot=badge]]:${BADGE_TEXT_SIZE} [&_[data-slot=badge]]:leading-tight`,\n      className,\n    ),\n    children,\n  };\n\n  const element = useRender({\n    defaultTagName: \"span\",\n    render,\n    props: mergeProps<\"span\">(defaultProps, props),\n  });\n\n  if (!children) return null;\n\n  return element;\n}\n\nexport {\n  Tree,\n  TreeItem,\n  TreeItemIcon,\n  TreeItemLabel,\n  TreeItemBadge,\n  type TreeVariant,\n  type TreeSize,\n  type TreeMode,\n};\n\nexport { TreeUtils };\n",
      "type": "registry:ui",
      "target": "components/ui/cubby-ui/tree/tree.tsx"
    },
    {
      "path": "registry/default/tree/lib/tree-utils.ts",
      "content": "import type { TreeNode } from \"../tree\";\n\n/**\n * Get all descendant IDs of a node (recursive)\n * @param node - The parent node\n * @returns Array of all descendant node IDs\n */\nexport function getAllDescendantIds<\n  TData extends Record<string, unknown> = Record<string, unknown>,\n>(node: TreeNode<TData>): string[] {\n  const ids: string[] = [];\n  if (node.children) {\n    for (const child of node.children) {\n      ids.push(child.id);\n      if (child.children && child.children.length > 0) {\n        ids.push(...getAllDescendantIds(child));\n      }\n    }\n  }\n  return ids;\n}\n\n/**\n * Get IDs of all leaf nodes (nodes without children)\n * @param nodes - Array of tree nodes\n * @returns Array of leaf node IDs\n */\nexport function getLeafNodeIds<\n  TData extends Record<string, unknown> = Record<string, unknown>,\n>(nodes: TreeNode<TData>[]): string[] {\n  const leafIds: string[] = [];\n  for (const node of nodes) {\n    if (!node.children || node.children.length === 0) {\n      leafIds.push(node.id);\n    } else {\n      leafIds.push(...getLeafNodeIds(node.children));\n    }\n  }\n  return leafIds;\n}\n\n/**\n * Build a map of child node IDs to their parent node IDs\n * @param nodes - The tree data array\n * @param parentMap - Map to populate (for internal use)\n * @param parentId - Current parent ID (for internal use)\n * @returns Map of child ID to parent ID\n */\nexport function buildParentMap<\n  TData extends Record<string, unknown> = Record<string, unknown>,\n>(\n  nodes: TreeNode<TData>[],\n  parentMap = new Map<string, string>(),\n  parentId?: string,\n): Map<string, string> {\n  for (const node of nodes) {\n    if (parentId) {\n      parentMap.set(node.id, parentId);\n    }\n    if (node.children) {\n      buildParentMap(node.children, parentMap, node.id);\n    }\n  }\n  return parentMap;\n}\n\n/**\n * Collect IDs of all visible nodes (including children of expanded nodes)\n * @param nodes - The tree data array\n * @param expandedNodes - Set of expanded node IDs\n * @param ids - Array to populate (for internal use)\n * @param disabledMap - Map to track disabled nodes (for internal use)\n * @returns Object with visible node IDs and disabled nodes map\n */\nexport function collectVisibleIds<\n  TData extends Record<string, unknown> = Record<string, unknown>,\n>(\n  nodes: TreeNode<TData>[],\n  expandedNodes: Set<string>,\n  ids: string[] = [],\n  disabledMap = new Map<string, boolean>(),\n): { visibleNodeIds: string[]; disabledNodesMap: Map<string, boolean> } {\n  for (const node of nodes) {\n    ids.push(node.id);\n    if (node.disabled) {\n      disabledMap.set(node.id, true);\n    }\n    if (node.children && expandedNodes.has(node.id)) {\n      collectVisibleIds(node.children, expandedNodes, ids, disabledMap);\n    }\n  }\n  return { visibleNodeIds: ids, disabledNodesMap: disabledMap };\n}\n\n/**\n * Handle keyboard navigation for tree nodes\n * @param e - Keyboard event\n * @param params - Navigation parameters\n */\nexport function handleTreeKeyboardNavigation<\n  TData extends Record<string, unknown> = Record<string, unknown>,\n>(\n  e: React.KeyboardEvent,\n  params: {\n    nodeId: string;\n    hasChildren: boolean;\n    isExpanded: boolean;\n    isDisabled: boolean;\n    node: TreeNode<TData>;\n    visibleNodeIds: string[];\n    disabledNodesMap: Map<string, boolean>;\n    parentNodeMap: Map<string, string>;\n    focusableNodes: React.MutableRefObject<Map<string, HTMLElement>>;\n    enableBulkActions: boolean;\n    disableSelection: boolean;\n    onToggleNode: (nodeId: string) => void;\n    onNodeSelect?: (nodeId: string) => void;\n    setLastFocusedNodeId: (nodeId: string | null) => void;\n    handleToggleChecked: () => void;\n  },\n): void {\n  const {\n    nodeId,\n    hasChildren,\n    isExpanded,\n    isDisabled,\n    node,\n    visibleNodeIds,\n    disabledNodesMap,\n    parentNodeMap,\n    focusableNodes,\n    enableBulkActions,\n    disableSelection,\n    onToggleNode,\n    onNodeSelect,\n    setLastFocusedNodeId,\n    handleToggleChecked,\n  } = params;\n\n  if (isDisabled) return;\n\n  const currentIndex = visibleNodeIds.indexOf(nodeId);\n\n  switch (e.key) {\n    case \" \":\n      e.preventDefault();\n      e.stopPropagation();\n      if (enableBulkActions) {\n        handleToggleChecked();\n      } else {\n        if (hasChildren) {\n          onToggleNode(nodeId);\n        }\n        if (!disableSelection) {\n          onNodeSelect?.(nodeId);\n        }\n      }\n      break;\n\n    case \"Enter\":\n      e.preventDefault();\n      e.stopPropagation();\n      if (enableBulkActions) {\n        if (hasChildren) {\n          onToggleNode(nodeId);\n        } else {\n          handleToggleChecked();\n        }\n      } else {\n        if (hasChildren) {\n          onToggleNode(nodeId);\n        }\n        if (!disableSelection) {\n          onNodeSelect?.(nodeId);\n        }\n      }\n      break;\n\n    case \"ArrowRight\":\n      e.preventDefault();\n      if (hasChildren) {\n        if (!isExpanded) {\n          onToggleNode(nodeId);\n        } else {\n          const firstChildId = node.children?.[0]?.id;\n          if (firstChildId && !disabledNodesMap.has(firstChildId)) {\n            const firstChildElement = focusableNodes.current.get(firstChildId);\n            if (firstChildElement) {\n              firstChildElement.focus();\n              setLastFocusedNodeId(firstChildId);\n            }\n          }\n        }\n      }\n      break;\n\n    case \"ArrowLeft\":\n      e.preventDefault();\n      if (hasChildren && isExpanded) {\n        onToggleNode(nodeId);\n      } else {\n        const parentId = parentNodeMap.get(nodeId);\n        if (parentId) {\n          const parentElement = focusableNodes.current.get(parentId);\n          if (parentElement) {\n            parentElement.focus();\n            setLastFocusedNodeId(parentId);\n          }\n        }\n      }\n      break;\n\n    case \"ArrowDown\": {\n      e.preventDefault();\n      let nextIndex = currentIndex + 1;\n      while (nextIndex < visibleNodeIds.length) {\n        const nextId = visibleNodeIds[nextIndex];\n        if (disabledNodesMap.has(nextId)) {\n          nextIndex++;\n          continue;\n        }\n        const nextElement = focusableNodes.current.get(nextId);\n        if (nextElement) {\n          nextElement.focus();\n          setLastFocusedNodeId(nextId);\n          break;\n        }\n        nextIndex++;\n      }\n      break;\n    }\n\n    case \"ArrowUp\": {\n      e.preventDefault();\n      let prevIndex = currentIndex - 1;\n      while (prevIndex >= 0) {\n        const prevId = visibleNodeIds[prevIndex];\n        if (disabledNodesMap.has(prevId)) {\n          prevIndex--;\n          continue;\n        }\n        const prevElement = focusableNodes.current.get(prevId);\n        if (prevElement) {\n          prevElement.focus();\n          setLastFocusedNodeId(prevId);\n          break;\n        }\n        prevIndex--;\n      }\n      break;\n    }\n\n    case \"Home\": {\n      e.preventDefault();\n      for (let i = 0; i < visibleNodeIds.length; i++) {\n        const firstId = visibleNodeIds[i];\n        if (disabledNodesMap.has(firstId)) continue;\n        const firstElement = focusableNodes.current.get(firstId);\n        if (firstElement) {\n          firstElement.focus();\n          setLastFocusedNodeId(firstId);\n          break;\n        }\n      }\n      break;\n    }\n\n    case \"End\": {\n      e.preventDefault();\n      for (let i = visibleNodeIds.length - 1; i >= 0; i--) {\n        const lastId = visibleNodeIds[i];\n        if (disabledNodesMap.has(lastId)) continue;\n        const lastElement = focusableNodes.current.get(lastId);\n        if (lastElement) {\n          lastElement.focus();\n          setLastFocusedNodeId(lastId);\n          break;\n        }\n      }\n      break;\n    }\n  }\n}\n",
      "type": "registry:lib",
      "target": "components/ui/cubby-ui/tree/lib/tree-utils.ts"
    }
  ],
  "type": "registry:ui"
}