{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "file-tree",
  "type": "registry:ui",
  "registryDependencies": [],
  "dependencies": ["@base-ui/react", "motion", "lucide-react"],
  "devDependencies": [],
  "files": [
    {
      "path": "file-tree.tsx",
      "content": "\"use client\";\n\nimport { Button as ButtonPrimitive } from \"@base-ui/react/button\";\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport {\n  File,\n  FileCode,\n  FileCog,\n  FileImage,\n  FileJson,\n  FileText,\n  Folder,\n  FolderOpen,\n  Loader2,\n} from \"lucide-react\";\nimport {\n  AnimatePresence,\n  motion,\n  type Transition,\n  useReducedMotion,\n} from \"motion/react\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst DEFAULT_HIGHLIGHT_COLOR = \"var(--color-brand, #3b82f6)\";\n\nconst itemTriggerClassName =\n  \"relative z-10 w-full cursor-pointer justify-start rounded-none border-0 bg-transparent p-0 text-start font-normal text-inherit shadow-none hover:bg-transparent focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50\";\n\nconst defaultFileTreeItemRender: NonNullable<\n  useRender.ComponentProps<\"button\">[\"render\"]\n> = (props) => <ButtonPrimitive {...props} />;\n\nconst instantTransition: Transition = { duration: 0 };\n\ntype HighlightBounds = {\n  top: number;\n  left: number;\n  width: number;\n  height: number;\n};\n\ntype RegisteredNode = {\n  buttonRef: React.RefObject<HTMLButtonElement | null>;\n  disabled: boolean;\n  isBranch: boolean;\n  label: string;\n  level: number;\n  nodeId: string;\n  parentId: string | null;\n  rowRef: React.RefObject<HTMLDivElement | null>;\n  siblingIndex: number;\n  siblingCount: number;\n};\n\ntype SiblingMeta = {\n  index: number;\n  count: number;\n};\n\ntype FileTreeContextValue = {\n  containerRef: React.RefObject<HTMLDivElement | null>;\n  collapseAll: () => void;\n  expandAll: () => void;\n  expandedIds: Set<string>;\n  focusNode: (nodeId: string) => void;\n  focusedNodeId: string | null;\n  getVisibleNodeIds: () => string[];\n  highlightBounds: HighlightBounds | null;\n  highlightColor: string;\n  iconMap: Record<string, React.ComponentType<{ className?: string }>>;\n  indentSize: number;\n  isNodeVisible: (nodeId: string) => boolean;\n  moveFocus: (direction: \"first\" | \"last\" | \"next\" | \"previous\") => void;\n  onItemKeyDown: (event: React.KeyboardEvent<HTMLButtonElement>) => void;\n  onLoadChildren?: (nodeId: string) => void;\n  onNodeClick?: (nodeId: string, event?: React.MouseEvent) => void;\n  onNodeContextMenu?: (nodeId: string, event: React.MouseEvent) => void;\n  onNodeDragEnd?: (\n    nodeId: string,\n    event: React.DragEvent<HTMLButtonElement>\n  ) => void;\n  onNodeDragOver?: (\n    nodeId: string,\n    event: React.DragEvent<HTMLButtonElement>\n  ) => void;\n  onNodeDragStart?: (\n    nodeId: string,\n    event: React.DragEvent<HTMLButtonElement>\n  ) => void;\n  onNodeDrop?: (\n    nodeId: string,\n    event: React.DragEvent<HTMLButtonElement>\n  ) => void;\n  onNodeExpand?: (nodeId: string, expanded: boolean) => void;\n  reduceMotion: boolean;\n  registerNode: (node: RegisteredNode) => void;\n  registryVersion: number;\n  searchQuery: string;\n  selectNode: (\n    nodeId: string,\n    options?: { additive?: boolean; range?: boolean }\n  ) => void;\n  selectedIds: Set<string>;\n  selectionMode: \"multiple\" | \"single\";\n  setFocusedNodeId: React.Dispatch<React.SetStateAction<string | null>>;\n  setHighlightBounds: React.Dispatch<\n    React.SetStateAction<HighlightBounds | null>\n  >;\n  setHighlightFromElement: (element: HTMLElement | null) => void;\n  showIcons: boolean;\n  toggleExpanded: (nodeId: string, expanded?: boolean) => void;\n  truncate: boolean;\n  unregisterNode: (nodeId: string) => void;\n};\n\ntype BranchContextValue = {\n  level: number;\n  parentId: string | null;\n};\n\nconst FileTreeContext = React.createContext<FileTreeContextValue | null>(null);\nconst FolderContext = React.createContext(false);\nconst BranchContext = React.createContext<BranchContextValue>({\n  level: 1,\n  parentId: null,\n});\nconst SiblingMetaContext = React.createContext<SiblingMeta>({\n  index: 0,\n  count: 1,\n});\n\nconst DEFAULT_EXT_ICONS: Record<\n  string,\n  React.ComponentType<{ className?: string }>\n> = {\n  tsx: FileCode,\n  ts: FileCode,\n  jsx: FileCode,\n  js: FileCode,\n  json: FileJson,\n  md: FileText,\n  mdx: FileText,\n  png: FileImage,\n  jpg: FileImage,\n  jpeg: FileImage,\n  svg: FileImage,\n  webp: FileImage,\n  css: FileCode,\n  config: FileCog,\n  toml: FileCog,\n  yaml: FileCog,\n  yml: FileCog,\n  env: FileCog,\n};\n\nconst FILENAME_ICONS: Record<\n  string,\n  React.ComponentType<{ className?: string }>\n> = {\n  dockerfile: FileCog,\n  makefile: FileCog,\n  \".env\": FileCog,\n  \".gitignore\": FileCog,\n};\n\nfunction useFileTree() {\n  const context = React.useContext(FileTreeContext);\n  if (!context) {\n    throw new Error(\"File tree components must be used within <FileTree />\");\n  }\n  return context;\n}\n\nfunction normalizeIds(ids?: string[] | Set<string>) {\n  if (!ids) {\n    return new Set<string>();\n  }\n  return ids instanceof Set ? ids : new Set(ids);\n}\n\nfunction setsEqual(left: Set<string>, right: Set<string>) {\n  if (left.size !== right.size) {\n    return false;\n  }\n  for (const value of left) {\n    if (!right.has(value)) {\n      return false;\n    }\n  }\n  return true;\n}\n\nfunction resolveFileIcon(\n  label: string,\n  iconMap: Record<string, React.ComponentType<{ className?: string }>>\n) {\n  const normalizedLabel = label.toLowerCase();\n\n  if (FILENAME_ICONS[normalizedLabel]) {\n    return FILENAME_ICONS[normalizedLabel];\n  }\n\n  if (normalizedLabel.startsWith(\".\")) {\n    const extensionless = normalizedLabel.slice(1);\n    if (iconMap[extensionless]) {\n      return iconMap[extensionless];\n    }\n  }\n\n  const segments = label.split(\".\");\n  if (segments.length > 2) {\n    const compound = segments.slice(-2).join(\".\").toLowerCase();\n    if (iconMap[compound]) {\n      return iconMap[compound];\n    }\n  }\n\n  const extension = segments.pop()?.toLowerCase() ?? \"\";\n  return iconMap[extension] ?? File;\n}\n\nfunction matchesSearch(label: string, query: string) {\n  return label.toLowerCase().includes(query.trim().toLowerCase());\n}\n\nfunction buildChildMap(nodes: Map<string, RegisteredNode>) {\n  const childMap = new Map<string | null, RegisteredNode[]>();\n\n  for (const node of nodes.values()) {\n    const siblings = childMap.get(node.parentId) ?? [];\n    siblings.push(node);\n    childMap.set(node.parentId, siblings);\n  }\n\n  for (const siblings of childMap.values()) {\n    siblings.sort((left, right) => left.siblingIndex - right.siblingIndex);\n  }\n\n  return childMap;\n}\n\nfunction collectMatchingNodeIds(\n  childMap: Map<string | null, RegisteredNode[]>,\n  query: string\n) {\n  const matching = new Set<string>();\n\n  const visit = (node: RegisteredNode): boolean => {\n    const children = childMap.get(node.nodeId) ?? [];\n    const childMatches = children.map(visit);\n    const selfMatches = matchesSearch(node.label, query);\n    const hasMatch = selfMatches || childMatches.some(Boolean);\n\n    if (hasMatch) {\n      matching.add(node.nodeId);\n    }\n\n    return hasMatch;\n  };\n\n  for (const node of childMap.get(null) ?? []) {\n    visit(node);\n  }\n\n  return matching;\n}\n\nfunction getVisibleNodeIdsFromRegistry(\n  nodes: Map<string, RegisteredNode>,\n  expandedIds: Set<string>,\n  searchQuery: string\n) {\n  const childMap = buildChildMap(nodes);\n  const trimmedSearch = searchQuery.trim();\n  const matchingIds = trimmedSearch\n    ? collectMatchingNodeIds(childMap, trimmedSearch)\n    : null;\n  const visible: string[] = [];\n\n  const visit = (node: RegisteredNode) => {\n    if (matchingIds && !matchingIds.has(node.nodeId)) {\n      return;\n    }\n\n    visible.push(node.nodeId);\n\n    if (!(node.isBranch && expandedIds.has(node.nodeId))) {\n      return;\n    }\n\n    for (const child of childMap.get(node.nodeId) ?? []) {\n      visit(child);\n    }\n  };\n\n  for (const node of childMap.get(null) ?? []) {\n    visit(node);\n  }\n\n  return visible;\n}\n\nfunction resolveMultipleSelection(\n  previous: Set<string>,\n  nodeId: string,\n  visibleIds: string[],\n  focusedNodeId: string | null,\n  options?: { additive?: boolean; range?: boolean }\n) {\n  const next = new Set(previous);\n\n  if (options?.range && focusedNodeId) {\n    const start = visibleIds.indexOf(focusedNodeId);\n    const end = visibleIds.indexOf(nodeId);\n\n    if (start !== -1 && end !== -1) {\n      const [from, to] = start < end ? [start, end] : [end, start];\n      for (const id of visibleIds.slice(from, to + 1)) {\n        next.add(id);\n      }\n      return next;\n    }\n  }\n\n  if (options?.additive) {\n    if (next.has(nodeId)) {\n      next.delete(nodeId);\n    } else {\n      next.add(nodeId);\n    }\n    return next;\n  }\n\n  return new Set([nodeId]);\n}\n\nfunction resolveSelectionUpdate(\n  previous: Set<string>,\n  nodeId: string,\n  visibleIds: string[],\n  focusedNodeId: string | null,\n  selectionMode: \"multiple\" | \"single\",\n  options?: { additive?: boolean; range?: boolean }\n) {\n  if (selectionMode === \"single\") {\n    return new Set([nodeId]);\n  }\n\n  return resolveMultipleSelection(\n    previous,\n    nodeId,\n    visibleIds,\n    focusedNodeId,\n    options\n  );\n}\n\nfunction expandSearchBranches(\n  nodes: Map<string, RegisteredNode>,\n  searchQuery: string\n) {\n  const trimmedSearch = searchQuery.trim();\n  if (!trimmedSearch) {\n    return [];\n  }\n\n  const childMap = buildChildMap(nodes);\n  const matchingIds = collectMatchingNodeIds(childMap, trimmedSearch);\n\n  return [...matchingIds].filter((nodeId) => nodes.get(nodeId)?.isBranch);\n}\n\ntype TreeItemKeyContext = {\n  currentNode: RegisteredNode | undefined;\n  expandedIds: Set<string>;\n  focusNode: (nodeId: string) => void;\n  moveFocus: (direction: \"first\" | \"last\" | \"next\" | \"previous\") => void;\n  nodes: Map<string, RegisteredNode>;\n  setExpandedIds: (updater: (previous: Set<string>) => Set<string>) => void;\n  toggleExpanded: (nodeId: string, expanded?: boolean) => void;\n};\n\nfunction handleTreeItemKeyWhenEmpty(\n  event: React.KeyboardEvent<HTMLButtonElement>,\n  moveFocus: TreeItemKeyContext[\"moveFocus\"]\n) {\n  if (event.key === \"ArrowDown\" || event.key === \"Home\") {\n    event.preventDefault();\n    moveFocus(event.key === \"Home\" ? \"first\" : \"next\");\n  }\n}\n\nfunction handleTreeItemArrowRight({\n  currentNode,\n  expandedIds,\n  moveFocus,\n  toggleExpanded,\n}: TreeItemKeyContext) {\n  if (!currentNode?.isBranch) {\n    return;\n  }\n\n  if (expandedIds.has(currentNode.nodeId)) {\n    moveFocus(\"next\");\n    return;\n  }\n\n  toggleExpanded(currentNode.nodeId, true);\n}\n\nfunction handleTreeItemArrowLeft({\n  currentNode,\n  expandedIds,\n  focusNode,\n  toggleExpanded,\n}: TreeItemKeyContext) {\n  if (!currentNode) {\n    return;\n  }\n\n  if (currentNode.isBranch && expandedIds.has(currentNode.nodeId)) {\n    toggleExpanded(currentNode.nodeId, false);\n    return;\n  }\n\n  if (currentNode.parentId) {\n    focusNode(currentNode.parentId);\n  }\n}\n\nfunction handleTreeItemStarKey({\n  currentNode,\n  nodes,\n  setExpandedIds,\n}: TreeItemKeyContext) {\n  if (!currentNode?.parentId) {\n    return;\n  }\n\n  setExpandedIds((previous) => {\n    const next = new Set(previous);\n\n    for (const node of nodes.values()) {\n      if (node.parentId === currentNode.parentId && node.isBranch) {\n        next.add(node.nodeId);\n      }\n    }\n\n    return next;\n  });\n}\n\nfunction handleTreeItemKeyDown(\n  event: React.KeyboardEvent<HTMLButtonElement>,\n  context: TreeItemKeyContext\n) {\n  const { currentNode, moveFocus } = context;\n\n  if (!currentNode) {\n    handleTreeItemKeyWhenEmpty(event, moveFocus);\n    return;\n  }\n\n  switch (event.key) {\n    case \"ArrowDown\":\n      event.preventDefault();\n      moveFocus(\"next\");\n      break;\n    case \"ArrowUp\":\n      event.preventDefault();\n      moveFocus(\"previous\");\n      break;\n    case \"ArrowRight\":\n      event.preventDefault();\n      handleTreeItemArrowRight(context);\n      break;\n    case \"ArrowLeft\":\n      event.preventDefault();\n      handleTreeItemArrowLeft(context);\n      break;\n    case \"Home\":\n      event.preventDefault();\n      moveFocus(\"first\");\n      break;\n    case \"End\":\n      event.preventDefault();\n      moveFocus(\"last\");\n      break;\n    case \"*\":\n      event.preventDefault();\n      handleTreeItemStarKey(context);\n      break;\n    default:\n      break;\n  }\n}\n\nfunction FileTreeHoverHighlight({\n  className,\n  reduceMotion,\n}: {\n  className?: string;\n  reduceMotion: boolean;\n}) {\n  const { highlightBounds } = useFileTree();\n\n  return (\n    <AnimatePresence>\n      {highlightBounds && (\n        <motion.div\n          animate={{\n            opacity: 1,\n            top: highlightBounds.top,\n            left: highlightBounds.left,\n            width: highlightBounds.width,\n            height: highlightBounds.height,\n          }}\n          className={className}\n          exit={{ opacity: 0 }}\n          initial={{ opacity: 0 }}\n          style={{ position: \"absolute\", pointerEvents: \"none\", zIndex: 0 }}\n          transition={\n            reduceMotion\n              ? instantTransition\n              : { type: \"spring\", stiffness: 500, damping: 40 }\n          }\n        />\n      )}\n    </AnimatePresence>\n  );\n}\n\nfunction useHighlightTarget() {\n  const { setHighlightFromElement } = useFileTree();\n  const ref = React.useRef<HTMLDivElement>(null);\n\n  const updateHighlight = React.useCallback(() => {\n    setHighlightFromElement(ref.current);\n  }, [setHighlightFromElement]);\n\n  return { ref, onFocus: updateHighlight, onMouseEnter: updateHighlight };\n}\n\nfunction FolderIcon({\n  closeIcon,\n  openIcon,\n  isOpen,\n  reduceMotion,\n}: {\n  closeIcon: React.ReactNode;\n  isOpen: boolean;\n  openIcon: React.ReactNode;\n  reduceMotion: boolean;\n}) {\n  if (reduceMotion) {\n    return (\n      <span className=\"inline-flex size-[1.125rem] shrink-0\">\n        {isOpen ? openIcon : closeIcon}\n      </span>\n    );\n  }\n\n  return (\n    <span className=\"relative inline-flex size-[1.125rem] shrink-0\">\n      <AnimatePresence initial={false} mode=\"popLayout\">\n        <motion.span\n          animate={{ scale: 1, opacity: 1, rotate: 0 }}\n          className=\"inline-flex\"\n          exit={{ scale: 0.95, opacity: 0, rotate: 15 }}\n          initial={{ scale: 0.95, opacity: 0, rotate: -15 }}\n          key={isOpen ? \"open\" : \"close\"}\n          transition={{\n            type: \"spring\",\n            stiffness: 500,\n            damping: 30,\n            mass: 0.8,\n          }}\n        >\n          {isOpen ? openIcon : closeIcon}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  );\n}\n\nfunction FolderContent({\n  children,\n  isOpen,\n  reduceMotion,\n}: {\n  children: React.ReactNode;\n  isOpen: boolean;\n  reduceMotion: boolean;\n}) {\n  if (reduceMotion) {\n    return isOpen ? <div>{children}</div> : null;\n  }\n\n  return (\n    <AnimatePresence initial={false}>\n      {isOpen && (\n        <motion.div\n          animate={{ height: \"auto\", opacity: 1 }}\n          exit={{ height: 0, opacity: 0 }}\n          initial={{ height: 0, opacity: 0 }}\n          style={{ overflow: \"hidden\" }}\n          transition={{ type: \"spring\", stiffness: 500, damping: 40 }}\n        >\n          {children}\n        </motion.div>\n      )}\n    </AnimatePresence>\n  );\n}\n\nfunction FileTreeRowContent({\n  highlight,\n  icon,\n  isBranch,\n  isOpen,\n  label,\n  loading,\n  openIcon,\n  truncate,\n}: {\n  highlight?: boolean;\n  icon?: React.ReactNode;\n  isBranch: boolean;\n  isOpen: boolean;\n  label: string;\n  loading?: boolean;\n  openIcon?: React.ReactNode;\n  truncate?: boolean;\n}) {\n  const { highlightColor, iconMap, reduceMotion, showIcons } = useFileTree();\n\n  const renderedIcon = loading ? (\n    <Loader2 className=\"size-4.5 animate-spin text-muted-foreground\" />\n  ) : isBranch ? (\n    <FolderIcon\n      closeIcon={icon ?? <Folder className=\"size-4.5\" />}\n      isOpen={isOpen}\n      openIcon={openIcon ?? <FolderOpen className=\"size-4.5\" />}\n      reduceMotion={reduceMotion}\n    />\n  ) : (\n    (icon ??\n    React.createElement(resolveFileIcon(label, iconMap), {\n      className: \"size-4.5\",\n    }))\n  );\n\n  return (\n    <div className=\"pointer-events-none flex min-w-0 items-center gap-2 p-2\">\n      {showIcons && (\n        <span className=\"inline-flex shrink-0\">{renderedIcon}</span>\n      )}\n      <span\n        className={cn(\"text-sm\", truncate && \"truncate\")}\n        style={highlight ? { color: highlightColor } : undefined}\n        title={truncate ? label : undefined}\n      >\n        {label}\n      </span>\n    </div>\n  );\n}\n\nfunction BranchSiblingProvider({ children }: { children: React.ReactNode }) {\n  const childArray = React.Children.toArray(children).filter(Boolean);\n  const count = childArray.length;\n\n  return (\n    <>\n      {childArray.map((child, index) => (\n        <SiblingMetaContext.Provider\n          key={\n            typeof child === \"object\" && child && \"key\" in child\n              ? String(child.key)\n              : index\n          }\n          value={{ index, count }}\n        >\n          {child}\n        </SiblingMetaContext.Provider>\n      ))}\n    </>\n  );\n}\n\nfunction getIndentStyle(indentSize: number): React.CSSProperties {\n  return { marginLeft: indentSize };\n}\n\nexport type FileTreeHandle = {\n  collapseAll: () => void;\n  expandAll: () => void;\n  focusNode: (nodeId: string) => void;\n};\n\nexport type FileTreeNodeData = {\n  children?: FileTreeNodeData[];\n  disabled?: boolean;\n  hasChildren?: boolean;\n  highlight?: boolean;\n  icon?: React.ReactNode;\n  id: string;\n  label: string;\n  loading?: boolean;\n  openIcon?: React.ReactNode;\n};\n\nexport type FileTreeProps = Omit<React.ComponentProps<\"div\">, \"onDragStart\"> & {\n  defaultExpandedIds?: string[];\n  defaultSelectedId?: string;\n  defaultSelectedIds?: string[];\n  expandedIds?: string[] | Set<string>;\n  highlightColor?: string;\n  iconMap?: Record<string, React.ComponentType<{ className?: string }>>;\n  indentSize?: number;\n  maxHeight?: number | string;\n  onExpandedIdsChange?: (expandedIds: string[]) => void;\n  onLoadChildren?: (nodeId: string) => void;\n  onNodeClick?: (nodeId: string, event?: React.MouseEvent) => void;\n  onNodeContextMenu?: (nodeId: string, event: React.MouseEvent) => void;\n  onNodeDragEnd?: (\n    nodeId: string,\n    event: React.DragEvent<HTMLButtonElement>\n  ) => void;\n  onNodeDragOver?: (\n    nodeId: string,\n    event: React.DragEvent<HTMLButtonElement>\n  ) => void;\n  onNodeDragStart?: (\n    nodeId: string,\n    event: React.DragEvent<HTMLButtonElement>\n  ) => void;\n  onNodeDrop?: (\n    nodeId: string,\n    event: React.DragEvent<HTMLButtonElement>\n  ) => void;\n  onNodeExpand?: (nodeId: string, expanded: boolean) => void;\n  onSelectedIdsChange?: (selectedIds: string[]) => void;\n  searchQuery?: string;\n  selectedIds?: string[] | Set<string>;\n  selectionMode?: \"multiple\" | \"single\";\n  showIcons?: boolean;\n  truncate?: boolean;\n};\n\nexport const FileTree = React.forwardRef<FileTreeHandle, FileTreeProps>(\n  function FileTree(\n    {\n      className,\n      children,\n      defaultExpandedIds = [],\n      defaultSelectedId,\n      defaultSelectedIds = [],\n      expandedIds: expandedIdsProp,\n      highlightColor = DEFAULT_HIGHLIGHT_COLOR,\n      iconMap,\n      indentSize = 24,\n      maxHeight,\n      onExpandedIdsChange,\n      onLoadChildren,\n      onNodeClick,\n      onNodeContextMenu,\n      onNodeDragEnd,\n      onNodeDragOver,\n      onNodeDragStart,\n      onNodeDrop,\n      onNodeExpand,\n      onSelectedIdsChange,\n      searchQuery = \"\",\n      selectedIds: selectedIdsProp,\n      selectionMode = \"single\",\n      showIcons = true,\n      truncate = false,\n      ...props\n    },\n    ref\n  ) {\n    const reduceMotion = useReducedMotion() === true;\n    const containerRef = React.useRef<HTMLDivElement>(null);\n    const nodesRef = React.useRef(new Map<string, RegisteredNode>());\n    const warnedDuplicateIdsRef = React.useRef(new Set<string>());\n    const registryFlushRef = React.useRef<number | null>(null);\n    const [highlightBounds, setHighlightBounds] =\n      React.useState<HighlightBounds | null>(null);\n    const [focusedNodeId, setFocusedNodeId] = React.useState<string | null>(\n      null\n    );\n    const [registryVersion, setRegistryVersion] = React.useState(0);\n\n    const isExpandedControlled = expandedIdsProp !== undefined;\n    const [uncontrolledExpandedIds, setUncontrolledExpandedIds] =\n      React.useState(() => normalizeIds(defaultExpandedIds));\n    const expandedIds = isExpandedControlled\n      ? normalizeIds(expandedIdsProp)\n      : uncontrolledExpandedIds;\n\n    const isSelectedControlled = selectedIdsProp !== undefined;\n    const [uncontrolledSelectedIds, setUncontrolledSelectedIds] =\n      React.useState(() => {\n        const ids = new Set(defaultSelectedIds);\n        if (defaultSelectedId) {\n          ids.add(defaultSelectedId);\n        }\n        return ids;\n      });\n    const selectedIds = isSelectedControlled\n      ? normalizeIds(selectedIdsProp)\n      : uncontrolledSelectedIds;\n\n    const mergedIconMap = React.useMemo(\n      () => ({ ...DEFAULT_EXT_ICONS, ...iconMap }),\n      [iconMap]\n    );\n\n    const scheduleRegistryFlush = React.useCallback(() => {\n      if (registryFlushRef.current !== null) {\n        return;\n      }\n\n      registryFlushRef.current = window.requestAnimationFrame(() => {\n        registryFlushRef.current = null;\n        setRegistryVersion((version) => version + 1);\n      });\n    }, []);\n\n    React.useEffect(() => {\n      return () => {\n        if (registryFlushRef.current !== null) {\n          window.cancelAnimationFrame(registryFlushRef.current);\n        }\n      };\n    }, []);\n\n    const setExpandedIds = React.useCallback(\n      (updater: (previous: Set<string>) => Set<string>) => {\n        if (isExpandedControlled) {\n          const next = updater(expandedIds);\n          if (!setsEqual(next, expandedIds)) {\n            onExpandedIdsChange?.([...next]);\n          }\n          return;\n        }\n\n        setUncontrolledExpandedIds((previous) => {\n          const next = updater(previous);\n          if (!setsEqual(next, previous)) {\n            onExpandedIdsChange?.([...next]);\n          }\n          return next;\n        });\n      },\n      [expandedIds, isExpandedControlled, onExpandedIdsChange]\n    );\n\n    const setSelectedIds = React.useCallback(\n      (updater: (previous: Set<string>) => Set<string>) => {\n        if (isSelectedControlled) {\n          const next = updater(selectedIds);\n          if (!setsEqual(next, selectedIds)) {\n            onSelectedIdsChange?.([...next]);\n          }\n          return;\n        }\n\n        setUncontrolledSelectedIds((previous) => {\n          const next = updater(previous);\n          if (!setsEqual(next, previous)) {\n            onSelectedIdsChange?.([...next]);\n          }\n          return next;\n        });\n      },\n      [isSelectedControlled, onSelectedIdsChange, selectedIds]\n    );\n\n    const setHighlightFromElement = React.useCallback(\n      (element: HTMLElement | null) => {\n        const container = containerRef.current;\n        if (!(element && container)) {\n          return;\n        }\n\n        const containerRect = container.getBoundingClientRect();\n        const elementRect = element.getBoundingClientRect();\n\n        setHighlightBounds({\n          top: elementRect.top - containerRect.top + container.scrollTop,\n          left: elementRect.left - containerRect.left + container.scrollLeft,\n          width: elementRect.width,\n          height: elementRect.height,\n        });\n      },\n      []\n    );\n\n    const registerNode = React.useCallback(\n      (node: RegisteredNode) => {\n        if (\n          nodesRef.current.has(node.nodeId) &&\n          !warnedDuplicateIdsRef.current.has(node.nodeId)\n        ) {\n          warnedDuplicateIdsRef.current.add(node.nodeId);\n          console.warn(\n            `[FileTree] Duplicate nodeId \"${node.nodeId}\" detected. Each FileTreeItem must use a unique nodeId.`\n          );\n        }\n\n        nodesRef.current.set(node.nodeId, node);\n        scheduleRegistryFlush();\n      },\n      [scheduleRegistryFlush]\n    );\n\n    const unregisterNode = React.useCallback(\n      (nodeId: string) => {\n        nodesRef.current.delete(nodeId);\n        scheduleRegistryFlush();\n      },\n      [scheduleRegistryFlush]\n    );\n\n    const visibleNodeIds = React.useMemo(() => {\n      if (registryVersion < 0) {\n        return [];\n      }\n\n      return getVisibleNodeIdsFromRegistry(\n        nodesRef.current,\n        expandedIds,\n        searchQuery\n      );\n    }, [expandedIds, registryVersion, searchQuery]);\n\n    const getVisibleNodeIds = React.useCallback(\n      () => visibleNodeIds,\n      [visibleNodeIds]\n    );\n\n    const trimmedSearch = searchQuery.trim();\n\n    React.useEffect(() => {\n      if (!trimmedSearch || registryVersion < 0) {\n        return;\n      }\n\n      const branchIds = expandSearchBranches(nodesRef.current, trimmedSearch);\n\n      if (branchIds.length === 0) {\n        return;\n      }\n\n      setExpandedIds((previous) => {\n        const next = new Set(previous);\n        let changed = false;\n\n        for (const nodeId of branchIds) {\n          if (!next.has(nodeId)) {\n            next.add(nodeId);\n            changed = true;\n          }\n        }\n\n        return changed ? next : previous;\n      });\n    }, [registryVersion, setExpandedIds, trimmedSearch]);\n\n    const isNodeVisible = React.useCallback(\n      (nodeId: string) => {\n        if (!trimmedSearch) {\n          return true;\n        }\n\n        if (!nodesRef.current.has(nodeId)) {\n          return true;\n        }\n\n        return visibleNodeIds.includes(nodeId);\n      },\n      [trimmedSearch, visibleNodeIds]\n    );\n\n    const toggleExpanded = React.useCallback(\n      (nodeId: string, expanded?: boolean) => {\n        setExpandedIds((previous) => {\n          const next = new Set(previous);\n          const isExpanded = next.has(nodeId);\n          const shouldExpand = expanded ?? !isExpanded;\n\n          if (shouldExpand) {\n            next.add(nodeId);\n          } else {\n            next.delete(nodeId);\n          }\n\n          if (shouldExpand !== isExpanded) {\n            onNodeExpand?.(nodeId, shouldExpand);\n            if (shouldExpand) {\n              onLoadChildren?.(nodeId);\n            }\n          }\n\n          return next;\n        });\n      },\n      [onLoadChildren, onNodeExpand, setExpandedIds]\n    );\n\n    const selectNode = React.useCallback(\n      (nodeId: string, options?: { additive?: boolean; range?: boolean }) => {\n        const visibleIds = getVisibleNodeIds();\n        if (!visibleIds.includes(nodeId)) {\n          return;\n        }\n\n        setSelectedIds((previous) =>\n          resolveSelectionUpdate(\n            previous,\n            nodeId,\n            visibleIds,\n            focusedNodeId,\n            selectionMode,\n            options\n          )\n        );\n      },\n      [focusedNodeId, getVisibleNodeIds, selectionMode, setSelectedIds]\n    );\n\n    const focusNode = React.useCallback(\n      (nodeId: string) => {\n        const node = nodesRef.current.get(nodeId);\n        if (!node || node.disabled) {\n          return;\n        }\n\n        setFocusedNodeId(nodeId);\n        node.buttonRef.current?.focus({ preventScroll: true });\n        setHighlightFromElement(node.rowRef.current);\n      },\n      [setHighlightFromElement]\n    );\n\n    const moveFocus = React.useCallback(\n      (direction: \"first\" | \"last\" | \"next\" | \"previous\") => {\n        const visibleIds = getVisibleNodeIds().filter((nodeId) => {\n          const node = nodesRef.current.get(nodeId);\n          return node && !node.disabled;\n        });\n\n        if (visibleIds.length === 0) {\n          return;\n        }\n\n        const currentIndex = focusedNodeId\n          ? visibleIds.indexOf(focusedNodeId)\n          : -1;\n\n        let nextIndex = currentIndex;\n\n        if (direction === \"first\") {\n          nextIndex = 0;\n        } else if (direction === \"last\") {\n          nextIndex = visibleIds.length - 1;\n        } else if (direction === \"next\") {\n          nextIndex =\n            currentIndex === -1\n              ? 0\n              : Math.min(currentIndex + 1, visibleIds.length - 1);\n        } else {\n          nextIndex =\n            currentIndex === -1\n              ? visibleIds.length - 1\n              : Math.max(currentIndex - 1, 0);\n        }\n\n        focusNode(visibleIds[nextIndex]);\n      },\n      [focusNode, focusedNodeId, getVisibleNodeIds]\n    );\n\n    const expandAll = React.useCallback(() => {\n      const branchIds = [...nodesRef.current.values()]\n        .filter((node) => node.isBranch)\n        .map((node) => node.nodeId);\n\n      setExpandedIds((previous) => {\n        const next = new Set(previous);\n        for (const nodeId of branchIds) {\n          next.add(nodeId);\n        }\n        return next;\n      });\n    }, [setExpandedIds]);\n\n    const collapseAll = React.useCallback(() => {\n      setExpandedIds(() => new Set());\n    }, [setExpandedIds]);\n\n    const onItemKeyDown = React.useCallback(\n      (event: React.KeyboardEvent<HTMLButtonElement>) => {\n        const visibleIds = getVisibleNodeIds();\n        const currentId =\n          focusedNodeId && visibleIds.includes(focusedNodeId)\n            ? focusedNodeId\n            : visibleIds[0];\n\n        handleTreeItemKeyDown(event, {\n          currentNode: currentId ? nodesRef.current.get(currentId) : undefined,\n          expandedIds,\n          focusNode,\n          moveFocus,\n          nodes: nodesRef.current,\n          setExpandedIds,\n          toggleExpanded,\n        });\n      },\n      [\n        expandedIds,\n        focusNode,\n        focusedNodeId,\n        getVisibleNodeIds,\n        moveFocus,\n        setExpandedIds,\n        toggleExpanded,\n      ]\n    );\n\n    const handleContainerScroll = React.useCallback(() => {\n      if (!focusedNodeId) {\n        return;\n      }\n\n      const node = nodesRef.current.get(focusedNodeId);\n      if (node?.rowRef.current) {\n        setHighlightFromElement(node.rowRef.current);\n      }\n    }, [focusedNodeId, setHighlightFromElement]);\n\n    React.useImperativeHandle(\n      ref,\n      () => ({ collapseAll, expandAll, focusNode }),\n      [collapseAll, expandAll, focusNode]\n    );\n\n    const contextValue = React.useMemo<FileTreeContextValue>(\n      () => ({\n        containerRef,\n        collapseAll,\n        expandAll,\n        expandedIds,\n        focusNode,\n        focusedNodeId,\n        getVisibleNodeIds,\n        highlightBounds,\n        highlightColor,\n        iconMap: mergedIconMap,\n        indentSize,\n        isNodeVisible,\n        moveFocus,\n        onItemKeyDown,\n        onLoadChildren,\n        onNodeClick,\n        onNodeContextMenu,\n        onNodeDragEnd,\n        onNodeDragOver,\n        onNodeDragStart,\n        onNodeDrop,\n        onNodeExpand,\n        reduceMotion,\n        registerNode,\n        registryVersion,\n        searchQuery,\n        selectNode,\n        selectedIds,\n        selectionMode,\n        setFocusedNodeId,\n        setHighlightBounds,\n        setHighlightFromElement,\n        showIcons,\n        toggleExpanded,\n        truncate,\n        unregisterNode,\n      }),\n      [\n        collapseAll,\n        expandAll,\n        expandedIds,\n        focusNode,\n        focusedNodeId,\n        getVisibleNodeIds,\n        highlightBounds,\n        highlightColor,\n        indentSize,\n        isNodeVisible,\n        mergedIconMap,\n        moveFocus,\n        onItemKeyDown,\n        onLoadChildren,\n        onNodeClick,\n        onNodeContextMenu,\n        onNodeDragEnd,\n        onNodeDragOver,\n        onNodeDragStart,\n        onNodeDrop,\n        onNodeExpand,\n        reduceMotion,\n        registerNode,\n        registryVersion,\n        searchQuery,\n        selectNode,\n        selectedIds,\n        selectionMode,\n        setHighlightFromElement,\n        showIcons,\n        toggleExpanded,\n        truncate,\n        unregisterNode,\n      ]\n    );\n\n    return (\n      <FileTreeContext.Provider value={contextValue}>\n        <div\n          className={cn(\n            \"overflow-hidden rounded-lg border border-border/60\",\n            className\n          )}\n          {...props}\n        >\n          <div\n            className={cn(\n              \"relative isolate w-full p-2\",\n              maxHeight !== undefined && \"overflow-y-auto\"\n            )}\n            onMouseLeave={() => setHighlightBounds(null)}\n            onScroll={handleContainerScroll}\n            ref={containerRef}\n            style={maxHeight !== undefined ? { maxHeight } : undefined}\n          >\n            <FileTreeHoverHighlight\n              className=\"z-0 rounded-lg border border-accent/45 bg-accent/55\"\n              reduceMotion={reduceMotion}\n            />\n            {children}\n          </div>\n        </div>\n      </FileTreeContext.Provider>\n    );\n  }\n);\n\nFileTree.displayName = \"FileTree\";\n\nexport function FileTreeList({\n  children,\n  className,\n  render,\n  ...props\n}: useRender.ComponentProps<\"div\">) {\n  return (\n    <BranchContext.Provider value={{ level: 1, parentId: null }}>\n      {useRender({\n        defaultTagName: \"div\",\n        props: mergeProps<\"div\">(\n          {\n            children: <BranchSiblingProvider>{children}</BranchSiblingProvider>,\n            className: cn(\"flex flex-col\", className),\n            role: \"tree\",\n          },\n          props\n        ),\n        render,\n        state: { slot: \"file-tree-list\" },\n      })}\n    </BranchContext.Provider>\n  );\n}\n\nexport type FileTreeItemProps = Omit<\n  useRender.ComponentProps<\"button\">,\n  \"children\"\n> & {\n  children?: React.ReactNode;\n  hasChildren?: boolean;\n  highlight?: boolean;\n  icon?: React.ReactNode;\n  openIcon?: React.ReactNode;\n  label: string;\n  loading?: boolean;\n  nodeId: string;\n  draggable?: boolean;\n};\n\nexport function FileTreeItem({\n  children,\n  className,\n  disabled = false,\n  draggable = false,\n  hasChildren,\n  highlight,\n  icon,\n  loading = false,\n  openIcon,\n  label,\n  nodeId,\n  onClick,\n  onContextMenu,\n  onDragEnd,\n  onDragOver,\n  onDragStart,\n  onDrop,\n  onFocus,\n  onKeyDown,\n  render,\n  ...props\n}: FileTreeItemProps) {\n  const {\n    expandedIds,\n    focusNode,\n    focusedNodeId,\n    getVisibleNodeIds,\n    indentSize,\n    isNodeVisible,\n    onItemKeyDown,\n    onLoadChildren,\n    onNodeClick,\n    onNodeContextMenu,\n    onNodeDragEnd,\n    onNodeDragOver,\n    onNodeDragStart,\n    onNodeDrop,\n    reduceMotion,\n    registerNode,\n    selectNode,\n    selectedIds,\n    selectionMode,\n    setFocusedNodeId,\n    toggleExpanded,\n    truncate,\n    unregisterNode,\n  } = useFileTree();\n\n  const { level, parentId } = React.useContext(BranchContext);\n  const { index: siblingIndex, count: siblingCount } =\n    React.useContext(SiblingMetaContext);\n\n  const buttonRef = React.useRef<HTMLButtonElement>(null);\n  const rowRef = React.useRef<HTMLDivElement>(null);\n  const highlightTarget = useHighlightTarget();\n\n  const childNodes = React.Children.toArray(children).filter(Boolean);\n  const isBranch =\n    hasChildren !== undefined ? hasChildren : childNodes.length > 0;\n  const isOpen = expandedIds.has(nodeId);\n  const isSelected = selectedIds.has(nodeId);\n  const isFocused = focusedNodeId === nodeId;\n  const visible = isNodeVisible(nodeId);\n  const visibleIds = getVisibleNodeIds();\n  const isFirstVisible = visibleIds[0] === nodeId;\n\n  React.useLayoutEffect(() => {\n    registerNode({\n      buttonRef,\n      disabled: Boolean(disabled),\n      isBranch,\n      label,\n      level,\n      nodeId,\n      parentId,\n      rowRef,\n      siblingIndex,\n      siblingCount,\n    });\n\n    return () => {\n      unregisterNode(nodeId);\n    };\n  }, [\n    disabled,\n    isBranch,\n    label,\n    level,\n    nodeId,\n    parentId,\n    registerNode,\n    siblingCount,\n    siblingIndex,\n    unregisterNode,\n  ]);\n\n  React.useEffect(() => {\n    if (isOpen && isBranch && childNodes.length === 0 && hasChildren) {\n      onLoadChildren?.(nodeId);\n    }\n  }, [\n    childNodes.length,\n    hasChildren,\n    isBranch,\n    isOpen,\n    nodeId,\n    onLoadChildren,\n  ]);\n\n  const handleActivate = React.useCallback(\n    (event: React.MouseEvent<HTMLButtonElement>) => {\n      if (disabled) {\n        return;\n      }\n\n      onClick?.(event);\n      if (event.defaultPrevented) {\n        return;\n      }\n\n      const additive =\n        selectionMode === \"multiple\" && (event.metaKey || event.ctrlKey);\n      const range = selectionMode === \"multiple\" && event.shiftKey;\n\n      if (isBranch && !additive && !range) {\n        toggleExpanded(nodeId);\n      }\n\n      selectNode(nodeId, { additive, range });\n      onNodeClick?.(nodeId, event);\n    },\n    [\n      disabled,\n      isBranch,\n      nodeId,\n      onClick,\n      onNodeClick,\n      selectNode,\n      selectionMode,\n      toggleExpanded,\n    ]\n  );\n\n  const handleFocus = React.useCallback(\n    (event: React.FocusEvent<HTMLButtonElement>) => {\n      onFocus?.(event);\n      if (disabled) {\n        return;\n      }\n      setFocusedNodeId(nodeId);\n      highlightTarget.onFocus();\n    },\n    [disabled, highlightTarget, nodeId, onFocus, setFocusedNodeId]\n  );\n\n  const handleKeyDown = React.useCallback(\n    (event: React.KeyboardEvent<HTMLButtonElement>) => {\n      onKeyDown?.(event);\n      if (event.defaultPrevented || disabled) {\n        return;\n      }\n      onItemKeyDown(event);\n    },\n    [disabled, onItemKeyDown, onKeyDown]\n  );\n\n  const row = (\n    <div\n      onMouseEnter={highlightTarget.onMouseEnter}\n      ref={(element) => {\n        rowRef.current = element;\n        highlightTarget.ref.current = element;\n      }}\n    >\n      <FileTreeRowContent\n        highlight={highlight}\n        icon={icon}\n        isBranch={isBranch}\n        isOpen={isOpen}\n        label={label}\n        loading={loading}\n        openIcon={openIcon}\n        truncate={truncate}\n      />\n    </div>\n  );\n\n  const itemButton = useRender({\n    defaultTagName: \"button\",\n    props: mergeProps<\"button\">(\n      {\n        \"aria-expanded\": isBranch ? isOpen : undefined,\n        \"aria-label\": label,\n        \"aria-level\": level,\n        \"aria-posinset\": siblingIndex + 1,\n        \"aria-selected\": isSelected,\n        \"aria-setsize\": siblingCount,\n        children: row,\n        className: cn(\n          itemTriggerClassName,\n          isSelected && \"bg-accent/45\",\n          className\n        ),\n        disabled,\n        draggable,\n        hidden: !visible,\n        onClick: handleActivate,\n        onContextMenu: (event: React.MouseEvent<HTMLButtonElement>) => {\n          onContextMenu?.(event);\n          onNodeContextMenu?.(nodeId, event);\n        },\n        onDoubleClick: () => {\n          if (isBranch) {\n            toggleExpanded(nodeId, true);\n            focusNode(nodeId);\n          }\n        },\n        onDragEnd: (event: React.DragEvent<HTMLButtonElement>) => {\n          onDragEnd?.(event);\n          onNodeDragEnd?.(nodeId, event);\n        },\n        onDragOver: (event: React.DragEvent<HTMLButtonElement>) => {\n          onDragOver?.(event);\n          onNodeDragOver?.(nodeId, event);\n        },\n        onDragStart: (event: React.DragEvent<HTMLButtonElement>) => {\n          onDragStart?.(event);\n          onNodeDragStart?.(nodeId, event);\n        },\n        onDrop: (event: React.DragEvent<HTMLButtonElement>) => {\n          onDrop?.(event);\n          onNodeDrop?.(nodeId, event);\n        },\n        onFocus: handleFocus,\n        onKeyDown: handleKeyDown,\n        ref: buttonRef,\n        role: \"treeitem\",\n        tabIndex:\n          !visible || disabled\n            ? -1\n            : isFocused || (!focusedNodeId && isFirstVisible)\n              ? 0\n              : -1,\n        type: \"button\",\n      },\n      props\n    ),\n    render: render ?? defaultFileTreeItemRender,\n    state: {\n      slot: \"file-tree-item\",\n      branch: isBranch,\n      expanded: isOpen,\n      selected: isSelected,\n    },\n  });\n\n  if (!isBranch) {\n    return (\n      <div data-value={nodeId} hidden={visible ? undefined : true}>\n        {itemButton}\n      </div>\n    );\n  }\n\n  return (\n    <FolderContext.Provider value={isOpen}>\n      <div data-value={nodeId} hidden={visible ? undefined : true}>\n        {itemButton}\n        <fieldset\n          className=\"relative m-0 min-w-0 border-0 p-0 before:absolute before:inset-y-0 before:-left-2 before:h-full before:w-px before:bg-border\"\n          style={getIndentStyle(indentSize)}\n        >\n          <FolderContent isOpen={isOpen} reduceMotion={reduceMotion}>\n            <BranchContext.Provider\n              value={{ level: level + 1, parentId: nodeId }}\n            >\n              <BranchSiblingProvider>{children}</BranchSiblingProvider>\n            </BranchContext.Provider>\n          </FolderContent>\n        </fieldset>\n      </div>\n    </FolderContext.Provider>\n  );\n}\n\nfunction FileTreeItemFromData({ item }: { item: FileTreeNodeData }) {\n  return (\n    <FileTreeItem\n      disabled={item.disabled}\n      hasChildren={item.hasChildren}\n      highlight={item.highlight}\n      icon={item.icon}\n      label={item.label}\n      loading={item.loading}\n      nodeId={item.id}\n      openIcon={item.openIcon}\n    >\n      {item.children?.map((child) => (\n        <FileTreeItemFromData item={child} key={child.id} />\n      ))}\n    </FileTreeItem>\n  );\n}\n\nexport type FileTreeFromItemsProps = FileTreeProps & {\n  items: FileTreeNodeData[];\n};\n\nexport const FileTreeFromItems = React.forwardRef<\n  FileTreeHandle,\n  FileTreeFromItemsProps\n>(function FileTreeFromItems({ items, children, ...treeProps }, ref) {\n  return (\n    <FileTree ref={ref} {...treeProps}>\n      <FileTreeList>\n        {items.map((item) => (\n          <FileTreeItemFromData item={item} key={item.id} />\n        ))}\n      </FileTreeList>\n      {children}\n    </FileTree>\n  );\n});\n\nFileTreeFromItems.displayName = \"FileTreeFromItems\";\n",
      "type": "registry:ui"
    }
  ],
  "title": "File Tree",
  "description": "Compound file tree with nested FileTreeItem rows, Base UI Button folder toggles, extension-aware icons, hover highlight tracking, and optional blue emphasis for new files."
}
