{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tree-view",
  "title": "Tree View",
  "description": "An outline of rows that open and close with disclosure triangles, with arrow-key navigation and focused and unfocused selection.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "@cupertinoui/macos-theme"
  ],
  "files": [
    {
      "path": "registry/macos/ui/tree-view.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronRightIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\ntype TreeViewContextValue = {\n  value: string | null\n  expanded: string[]\n  // The item that Tab moves focus to: the last one focused.\n  active: string | null\n  select: (value: string) => void\n  toggle: (value: string, open?: boolean) => void\n  setActive: (value: string) => void\n}\n\nconst TreeViewContext = React.createContext<TreeViewContextValue | null>(null)\nconst TreeViewLevelContext = React.createContext(1)\n\nfunction useTreeView() {\n  const context = React.useContext(TreeViewContext)\n  if (!context) throw new Error(\"TreeViewItem must be used within a TreeView.\")\n  return context\n}\n\nfunction itemsOf(root: HTMLElement | null) {\n  return Array.from(root?.querySelectorAll<HTMLElement>('[role=\"treeitem\"]') ?? [])\n}\n\n// An outline view: rows that open and close with a disclosure triangle,\n// indented by level, like the file tree in Xcode's navigator or Finder's\n// list view. Select with a click or the arrow keys; Right and Left open and\n// close a row, and a double-click toggles it. The selection is accent blue\n// while the tree has focus and grey when it doesn't.\nfunction TreeView({\n  className,\n  ref,\n  value: valueProp,\n  defaultValue = null,\n  onValueChange,\n  expanded: expandedProp,\n  defaultExpanded = [],\n  onExpandedChange,\n  onKeyDown,\n  children,\n  ...props\n}: Omit<React.ComponentProps<\"div\">, \"defaultValue\"> & {\n  value?: string | null\n  defaultValue?: string | null\n  onValueChange?: (value: string) => void\n  expanded?: string[]\n  defaultExpanded?: string[]\n  onExpandedChange?: (expanded: string[]) => void\n}) {\n  const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue)\n  const [uncontrolledExpanded, setUncontrolledExpanded] = React.useState(defaultExpanded)\n  const [active, setActive] = React.useState<string | null>(null)\n  const root = React.useRef<HTMLDivElement | null>(null)\n  const value = valueProp === undefined ? uncontrolledValue : valueProp\n  const expanded = expandedProp ?? uncontrolledExpanded\n\n  const context = React.useMemo<TreeViewContextValue>(\n    () => ({\n      value,\n      expanded,\n      active,\n      setActive,\n      select(next) {\n        if (valueProp === undefined) setUncontrolledValue(next)\n        onValueChange?.(next)\n      },\n      toggle(item, open = !expanded.includes(item)) {\n        if (open === expanded.includes(item)) return\n        const next = open ? [...expanded, item] : expanded.filter((entry) => entry !== item)\n        if (expandedProp === undefined) setUncontrolledExpanded(next)\n        onExpandedChange?.(next)\n      },\n    }),\n    [value, expanded, active, valueProp, expandedProp, onValueChange, onExpandedChange]\n  )\n\n  // Keep one row reachable with Tab, also after the one focused last was\n  // closed away or removed.\n  React.useEffect(() => {\n    const values = itemsOf(root.current).map((item) => item.dataset.value)\n    if (active !== null && values.includes(active)) return\n    const next = value !== null && values.includes(value) ? value : values[0]\n    if (next !== undefined && next !== active) setActive(next)\n  }, [active, value, expanded, children])\n\n  return (\n    <TreeViewContext.Provider value={context}>\n      <div\n        ref={(node) => {\n          root.current = node\n          if (typeof ref === \"function\") return ref(node)\n          if (ref) ref.current = node\n        }}\n        role=\"tree\"\n        data-slot=\"tree-view\"\n        className={cn(\"group/tree-view flex min-w-0 flex-col text-mac-body text-mac-label\", className)}\n        onKeyDown={(event) => {\n          onKeyDown?.(event)\n          const items = itemsOf(root.current)\n          const current = (event.target as HTMLElement).closest<HTMLElement>('[role=\"treeitem\"]')\n          const index = current ? items.indexOf(current) : -1\n          if (!current || index < 0 || event.defaultPrevented) return\n          const item = current.dataset.value ?? \"\"\n          const level = Number(current.getAttribute(\"aria-level\"))\n          const open = current.getAttribute(\"aria-expanded\")\n          let target: HTMLElement | undefined\n          switch (event.key) {\n            case \"ArrowDown\":\n              target = items[index + 1]\n              break\n            case \"ArrowUp\":\n              target = items[index - 1]\n              break\n            case \"Home\":\n              target = items[0]\n              break\n            case \"End\":\n              target = items.at(-1)\n              break\n            case \"ArrowRight\":\n              if (open === \"false\") context.toggle(item, true)\n              else if (open === \"true\") target = items[index + 1]\n              break\n            case \"ArrowLeft\":\n              if (open === \"true\") context.toggle(item, false)\n              else target = items.slice(0, index).findLast((entry) => Number(entry.getAttribute(\"aria-level\")) < level)\n              break\n            case \"Enter\":\n            case \" \":\n              context.select(item)\n              break\n            default:\n              return\n          }\n          event.preventDefault()\n          if (!target) return\n          target.focus()\n          // Like an outline view, the selection follows the arrow keys.\n          context.select(target.dataset.value ?? \"\")\n        }}\n        {...props}\n      >\n        {children}\n      </div>\n    </TreeViewContext.Provider>\n  )\n}\n\n// A row of the tree. Nest TreeViewItems inside it to give it a disclosure\n// triangle; they show while it's open.\nfunction TreeViewItem({\n  className,\n  value,\n  label,\n  icon,\n  badge,\n  children,\n  onClick,\n  onDoubleClick,\n  onFocus,\n  style,\n  ...props\n}: Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  value: string\n  label: React.ReactNode\n  icon?: React.ReactNode\n  badge?: React.ReactNode\n  children?: React.ReactNode\n}) {\n  const context = useTreeView()\n  const level = React.useContext(TreeViewLevelContext)\n  const branch = React.Children.toArray(children).length > 0\n  const open = branch && context.expanded.includes(value)\n\n  return (\n    <>\n      <div\n        role=\"treeitem\"\n        data-slot=\"tree-view-item\"\n        data-value={value}\n        aria-level={level}\n        aria-expanded={branch ? open : undefined}\n        aria-selected={context.value === value}\n        tabIndex={(context.active ?? context.value) === value ? 0 : -1}\n        style={{ \"--tree-view-level\": level, ...style } as React.CSSProperties}\n        className={cn(\n          \"group/tree-view-item flex min-h-[22px] shrink-0 cursor-default items-center gap-1.5 rounded-[5px] pr-2 pl-[calc(var(--tree-view-level)*16px_-_12px)] outline-none select-none aria-selected:bg-mac-selection-unfocused group-focus-within/tree-view:aria-selected:bg-mac-selection group-focus-within/tree-view:aria-selected:text-white [&:focus-visible:not([aria-selected=true])]:mac-focus-ring\",\n          className\n        )}\n        onClick={(event) => {\n          onClick?.(event)\n          if (!event.defaultPrevented) context.select(value)\n        }}\n        onDoubleClick={(event) => {\n          onDoubleClick?.(event)\n          if (branch && !event.defaultPrevented) context.toggle(value)\n        }}\n        onFocus={(event) => {\n          onFocus?.(event)\n          context.setActive(value)\n        }}\n        {...props}\n      >\n        <span\n          data-slot=\"tree-view-item-disclosure\"\n          aria-hidden=\"true\"\n          onClick={(event) => {\n            if (!branch) return\n            event.stopPropagation()\n            context.toggle(value)\n          }}\n          className=\"flex size-3 shrink-0 items-center justify-center text-mac-label-secondary group-focus-within/tree-view:group-aria-selected/tree-view-item:text-white/80\"\n        >\n          {branch && (\n            <ChevronRightIcon\n              className={cn(\"size-3 transition-transform\", open && \"rotate-90\")}\n              strokeWidth={2.4}\n            />\n          )}\n        </span>\n        {icon && (\n          <span\n            data-slot=\"tree-view-item-icon\"\n            className=\"flex shrink-0 items-center [&_svg:not([class*='size-'])]:size-4\"\n          >\n            {icon}\n          </span>\n        )}\n        <span data-slot=\"tree-view-item-label\" className=\"min-w-0 flex-1 truncate\">\n          {label}\n        </span>\n        {badge !== undefined && (\n          <span\n            data-slot=\"tree-view-item-badge\"\n            className=\"flex shrink-0 items-center text-mac-subheadline text-mac-label-secondary tabular-nums group-focus-within/tree-view:group-aria-selected/tree-view-item:text-white/80\"\n          >\n            {badge}\n          </span>\n        )}\n      </div>\n      {open && (\n        <TreeViewLevelContext.Provider value={level + 1}>\n          {children}\n        </TreeViewLevelContext.Provider>\n      )}\n    </>\n  )\n}\n\nexport { TreeView, TreeViewItem }\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}