{
  "slug": "file-tree",
  "name": "File Tree",
  "description": "Smooth, interactive file tree component with guide lines, nested folder expansion, selection, and keyboard navigation.",
  "summary": "A file tree component for developer tools, file explorers, IDE sidebars and hierarchical directory structures. Built with Motion height transitions, pixel-aligned guide lines, chevron rotation, single and multi-selection, and keyboard expand, collapse and select on each node.",
  "category": "navigation",
  "tags": [
    "file-tree",
    "tree",
    "navigation",
    "directory",
    "explorer",
    "sidebar",
    "motion",
    "interactive"
  ],
  "dependencies": [
    "clsx",
    "tailwind-merge",
    "motion",
    "lucide-react"
  ],
  "version": "1.0.0",
  "createdDate": "2026-09-22",
  "updatedDate": "2026-09-22",
  "interactive": true,
  "supportsColor": false,
  "highlights": [
    "Smooth height expansion and 150ms chevron rotation",
    "Pixel-aligned vertical guide lines for deep tree hierarchies",
    "Clean borderless design optimized for dark and light theme sidebars",
    "Controlled and uncontrolled node expansion and selection states",
    "Keyboard support: ArrowRight and ArrowLeft expand and collapse, Enter and Space select or toggle"
  ],
  "anatomy": [
    "<TreeView> (Root container managing tree state, keyboard handlers, and layout)",
    "<TreeNode> (Hierarchical node item rendering chevrons, icons, labels, and children)"
  ],
  "physics": {
    "engine": "Motion Height Transitions",
    "description": "Height expansion curves [0.16, 1, 0.3, 1] with 150ms chevron rotation.",
    "parameters": [
      {
        "label": "Height Expansion",
        "value": "200ms cubic-bezier(0.16, 1, 0.3, 1)"
      },
      {
        "label": "Chevron Rotation",
        "value": "150ms ease-in-out"
      },
      {
        "label": "Row Hover Feedback",
        "value": "150ms transition-colors"
      },
      {
        "label": "Indentation Step",
        "value": "16px default pitch"
      }
    ]
  },
  "accessibility": {
    "role": "tree",
    "aria": "Uses role='tree' and role='treeitem' with live aria-expanded and aria-selected state management.",
    "reducedMotion": "Instantaneous node expansion without motion delay when prefers-reduced-motion is active.",
    "keyboard": [
      {
        "key": "Tab / Shift+Tab",
        "description": "Move focus into and out of the tree."
      },
      {
        "key": "Arrow keys",
        "description": "Move between nodes; ArrowRight expands and ArrowLeft collapses the focused folder."
      },
      {
        "key": "Enter / Space",
        "description": "Select the focused node (and toggle its folder when expandOnFolderClick is true). Hold Ctrl/Cmd with multiSelect to add or remove it from the selection."
      }
    ]
  },
  "guidelines": {
    "recommended": [
      "IDE sidebars, project file browsers, and file manager interfaces",
      "Hierarchical documentation tables of contents and nested category trees",
      "Directory structure visualization in developer toolkits"
    ],
    "bestPractices": [
      "Set expandOnFolderClick=true for intuitive folder opening experience",
      "Provide unique id properties for each node in the data tree",
      "Use showLines=true to aid visual depth tracking in multi-level trees"
    ]
  },
  "props": [
    {
      "name": "data",
      "type": "TreeNode[]",
      "required": true,
      "description": "Array of tree nodes representing files, folders, and nested children."
    },
    {
      "name": "className",
      "type": "string",
      "description": "Additional CSS classes for the tree container."
    },
    {
      "name": "onNodeClick",
      "type": "(node: TreeNode) => void",
      "description": "Called when a node is clicked or activated with the keyboard."
    },
    {
      "name": "onNodeExpand",
      "type": "(nodeId: string, expanded: boolean) => void",
      "description": "Called when a folder is expanded or collapsed."
    },
    {
      "name": "defaultExpandedIds",
      "type": "string[]",
      "defaultValue": "[]",
      "description": "Folder ids expanded on mount when uncontrolled."
    },
    {
      "name": "expandedIds",
      "type": "string[]",
      "description": "Controlled list of expanded folder ids."
    },
    {
      "name": "onExpandedIdsChange",
      "type": "(expandedIds: string[]) => void",
      "description": "Called with the new list of expanded folder ids."
    },
    {
      "name": "showLines",
      "type": "boolean",
      "defaultValue": "true",
      "description": "Render vertical guide lines for visual depth tracking."
    },
    {
      "name": "showIcons",
      "type": "boolean",
      "defaultValue": "true",
      "description": "Display folder and file icons next to node labels."
    },
    {
      "name": "selectable",
      "type": "boolean",
      "defaultValue": "true",
      "description": "Enable node selection on click."
    },
    {
      "name": "multiSelect",
      "type": "boolean",
      "defaultValue": "false",
      "description": "Allow selecting multiple nodes with Ctrl/Cmd key."
    },
    {
      "name": "selectedIds",
      "type": "string[]",
      "description": "Controlled list of selected node ids."
    },
    {
      "name": "defaultSelectedIds",
      "type": "string[]",
      "defaultValue": "[]",
      "description": "Node ids selected on mount when uncontrolled."
    },
    {
      "name": "onSelectionChange",
      "type": "(selectedIds: string[]) => void",
      "description": "Called with the new list of selected node ids."
    },
    {
      "name": "indent",
      "type": "number",
      "defaultValue": "16",
      "description": "Indentation width in pixels per level."
    },
    {
      "name": "animateExpand",
      "type": "boolean",
      "defaultValue": "true",
      "description": "Enable smooth motion animation during folder expansion."
    },
    {
      "name": "expandOnFolderClick",
      "type": "boolean",
      "defaultValue": "true",
      "description": "Toggles a folder when its row is clicked, not only its chevron."
    }
  ],
  "usage": "\"use client\";\n\nimport { FileTree, type TreeNode } from \"@/components/ui/file-tree\";\n\nconst data: TreeNode[] = [\n  {\n    id: \"src\",\n    label: \"src\",\n    children: [\n      { id: \"app\", label: \"app.tsx\" },\n      { id: \"utils\", label: \"utils.ts\" },\n    ],\n  },\n  { id: \"readme\", label: \"README.md\" },\n];\n\nexport function FileTreeDemo() {\n  return (\n    <FileTree\n      data={data}\n      defaultExpandedIds={[\"src\"]}\n      onNodeClick={(node) => console.log(node.id)}\n    />\n  );\n}",
  "files": [
    {
      "name": "file-tree.tsx",
      "path": "registry/ui/file-tree.tsx",
      "code": "\"use client\";\n\nimport React, { useState, useCallback, useMemo, useRef } from \"react\";\nimport { ChevronRight, Folder, FolderOpen, File } from \"lucide-react\";\nimport { motion, AnimatePresence, useReducedMotion } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type TreeNode = {\n  id: string;\n  label: string;\n  icon?: React.ReactNode;\n  children?: TreeNode[];\n  data?: unknown;\n  disabled?: boolean;\n};\n\nexport type TreeViewProps = {\n  data: TreeNode[];\n  className?: string;\n  onNodeClick?: (node: TreeNode) => void;\n  onNodeExpand?: (nodeId: string, expanded: boolean) => void;\n  defaultExpandedIds?: string[];\n  expandedIds?: string[];\n  onExpandedIdsChange?: (expandedIds: string[]) => void;\n  showLines?: boolean;\n  showIcons?: boolean;\n  selectable?: boolean;\n  multiSelect?: boolean;\n  selectedIds?: string[];\n  defaultSelectedIds?: string[];\n  onSelectionChange?: (selectedIds: string[]) => void;\n  indent?: number;\n  animateExpand?: boolean;\n  expandOnFolderClick?: boolean;\n};\n\nexport function TreeView({\n  data,\n  className,\n  onNodeClick,\n  onNodeExpand,\n  defaultExpandedIds = [],\n  expandedIds: controlledExpandedIds,\n  onExpandedIdsChange,\n  showLines = true,\n  showIcons = true,\n  selectable = true,\n  multiSelect = false,\n  selectedIds: controlledSelectedIds,\n  defaultSelectedIds = [],\n  onSelectionChange,\n  indent = 16,\n  animateExpand = true,\n  expandOnFolderClick = true,\n}: TreeViewProps) {\n  const prefersReducedMotion = useReducedMotion();\n\n  const [internalExpandedIds, setInternalExpandedIds] = useState<Set<string>>(\n    () => new Set(defaultExpandedIds),\n  );\n\n  const isExpandedControlled = controlledExpandedIds !== undefined;\n  const currentExpandedSet = useMemo(() => {\n    if (isExpandedControlled) {\n      return new Set(controlledExpandedIds);\n    }\n    return internalExpandedIds;\n  }, [isExpandedControlled, controlledExpandedIds, internalExpandedIds]);\n\n  const toggleExpanded = useCallback(\n    (nodeId: string) => {\n      const nextSet = new Set(currentExpandedSet);\n      const isExpanded = nextSet.has(nodeId);\n      if (isExpanded) {\n        nextSet.delete(nodeId);\n      } else {\n        nextSet.add(nodeId);\n      }\n\n      if (!isExpandedControlled) {\n        setInternalExpandedIds(nextSet);\n      }\n\n      onExpandedIdsChange?.(Array.from(nextSet));\n      onNodeExpand?.(nodeId, !isExpanded);\n    },\n    [\n      currentExpandedSet,\n      isExpandedControlled,\n      onExpandedIdsChange,\n      onNodeExpand,\n    ],\n  );\n\n  const [internalSelectedIds, setInternalSelectedIds] =\n    useState<string[]>(defaultSelectedIds);\n\n  const isSelectedControlled = controlledSelectedIds !== undefined;\n  const currentSelectedIds = isSelectedControlled\n    ? controlledSelectedIds\n    : internalSelectedIds;\n\n  const handleSelection = useCallback(\n    (nodeId: string, isCtrlKey: boolean) => {\n      if (!selectable) return;\n\n      let nextSelection: string[];\n\n      if (multiSelect && isCtrlKey) {\n        nextSelection = currentSelectedIds.includes(nodeId)\n          ? currentSelectedIds.filter((id) => id !== nodeId)\n          : [...currentSelectedIds, nodeId];\n      } else {\n        nextSelection = currentSelectedIds.includes(nodeId) ? [] : [nodeId];\n      }\n\n      if (!isSelectedControlled) {\n        setInternalSelectedIds(nextSelection);\n      }\n\n      onSelectionChange?.(nextSelection);\n    },\n    [\n      selectable,\n      multiSelect,\n      currentSelectedIds,\n      isSelectedControlled,\n      onSelectionChange,\n    ],\n  );\n\n  const visibleNodes = useMemo(() => {\n    const list: { node: TreeNode; parentId: string | null }[] = [];\n    const walk = (nodes: TreeNode[], parentId: string | null) => {\n      for (const node of nodes) {\n        list.push({ node, parentId });\n        if (node.children?.length && currentExpandedSet.has(node.id)) {\n          walk(node.children, node.id);\n        }\n      }\n    };\n    walk(data, null);\n    return list;\n  }, [data, currentExpandedSet]);\n\n  const itemRefs = useRef(new Map<string, HTMLDivElement>());\n  const [focusedId, setFocusedId] = useState<string | null>(null);\n\n  const tabStopId = useMemo(() => {\n    const visibleIds = visibleNodes.map(({ node }) => node.id);\n    if (focusedId && visibleIds.includes(focusedId)) return focusedId;\n    return (\n      visibleIds.find((id) => currentSelectedIds.includes(id)) ??\n      visibleIds[0] ??\n      null\n    );\n  }, [visibleNodes, focusedId, currentSelectedIds]);\n\n  const focusNode = useCallback((nodeId: string | null | undefined) => {\n    if (!nodeId) return;\n    setFocusedId(nodeId);\n    itemRefs.current.get(nodeId)?.focus();\n  }, []);\n\n  const moveFocus = useCallback(\n    (nodeId: string, key: string) => {\n      const index = visibleNodes.findIndex(({ node }) => node.id === nodeId);\n      if (index === -1) return false;\n      const current = visibleNodes[index];\n      const hasChildren = (current.node.children?.length ?? 0) > 0;\n      const isExpanded = currentExpandedSet.has(nodeId);\n\n      switch (key) {\n        case \"ArrowDown\":\n          focusNode(visibleNodes[index + 1]?.node.id);\n          return true;\n        case \"ArrowUp\":\n          focusNode(visibleNodes[index - 1]?.node.id);\n          return true;\n        case \"Home\":\n          focusNode(visibleNodes[0]?.node.id);\n          return true;\n        case \"End\":\n          focusNode(visibleNodes[visibleNodes.length - 1]?.node.id);\n          return true;\n        case \"ArrowRight\":\n          if (!hasChildren) return false;\n          if (isExpanded) focusNode(current.node.children?.[0]?.id);\n          else toggleExpanded(nodeId);\n          return true;\n        case \"ArrowLeft\":\n          if (hasChildren && isExpanded) toggleExpanded(nodeId);\n          else focusNode(current.parentId);\n          return true;\n        default:\n          return false;\n      }\n    },\n    [visibleNodes, currentExpandedSet, focusNode, toggleExpanded],\n  );\n\n  const handleNodeClick = useCallback(\n    (node: TreeNode, e: React.MouseEvent) => {\n      e.stopPropagation();\n      if (node.disabled) return;\n\n      const hasChildren = (node.children?.length ?? 0) > 0;\n      if (hasChildren && expandOnFolderClick) {\n        toggleExpanded(node.id);\n      }\n\n      handleSelection(node.id, e.ctrlKey || e.metaKey);\n      onNodeClick?.(node);\n    },\n    [expandOnFolderClick, toggleExpanded, handleSelection, onNodeClick],\n  );\n\n  const renderNode = (node: TreeNode, level = 0) => {\n    const hasChildren = (node.children?.length ?? 0) > 0;\n    const isExpanded = currentExpandedSet.has(node.id);\n    const isSelected = currentSelectedIds.includes(node.id);\n\n    return (\n      <div key={node.id} className=\"relative\">\n        <div\n          ref={(element) => {\n            if (element) itemRefs.current.set(node.id, element);\n            else itemRefs.current.delete(node.id);\n          }}\n          role=\"treeitem\"\n          aria-level={level + 1}\n          aria-expanded={hasChildren ? isExpanded : undefined}\n          aria-selected={selectable ? isSelected : undefined}\n          aria-disabled={node.disabled || undefined}\n          tabIndex={node.id === tabStopId ? 0 : -1}\n          onFocus={() => setFocusedId(node.id)}\n          className={cn(\n            \"group/node relative flex items-center h-8 px-2 rounded-md cursor-pointer select-none transition-colors duration-150 outline-none focus-visible:ring-1 focus-visible:ring-zinc-400 dark:focus-visible:ring-zinc-600\",\n            isSelected\n              ? \"bg-zinc-100 dark:bg-zinc-800/80 text-zinc-900 dark:text-zinc-100 font-medium\"\n              : \"text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100/70 dark:hover:bg-zinc-800/50 hover:text-zinc-900 dark:hover:text-zinc-100\",\n            node.disabled && \"opacity-50 pointer-events-none\",\n          )}\n          style={{ paddingLeft: `${level * indent + 8}px` }}\n          onClick={(e) => handleNodeClick(node, e)}\n          onKeyDown={(e) => {\n            if (e.key === \"Enter\" || e.key === \" \") {\n              e.preventDefault();\n              handleNodeClick(node, e as unknown as React.MouseEvent);\n            } else if (moveFocus(node.id, e.key)) {\n              e.preventDefault();\n            }\n          }}\n        >\n          {showLines && level > 0 && (\n            <div className=\"absolute left-0 top-0 bottom-0 pointer-events-none\">\n              {Array.from({ length: level }).map((_, i) => (\n                <div\n                  key={i}\n                  className=\"absolute top-0 bottom-0 border-l border-zinc-200 dark:border-zinc-800/80\"\n                  style={{ left: `${i * indent + 15}px` }}\n                />\n              ))}\n            </div>\n          )}\n\n          <div className=\"w-4 h-4 flex items-center justify-center shrink-0 mr-1\">\n            {hasChildren ? (\n              <motion.div\n                animate={{ rotate: isExpanded ? 90 : 0 }}\n                transition={\n                  prefersReducedMotion || !animateExpand\n                    ? { duration: 0 }\n                    : { duration: 0.15, ease: \"easeInOut\" }\n                }\n                className=\"flex items-center justify-center text-zinc-400 group-hover/node:text-zinc-600 dark:group-hover/node:text-zinc-300\"\n                onClick={(e) => {\n                  if (!expandOnFolderClick) {\n                    e.stopPropagation();\n                    toggleExpanded(node.id);\n                  }\n                }}\n              >\n                <ChevronRight className=\"h-3.5 w-3.5 shrink-0\" />\n              </motion.div>\n            ) : (\n              <span className=\"w-3.5 h-3.5\" />\n            )}\n          </div>\n\n          {showIcons && (\n            <div className=\"w-4 h-4 flex items-center justify-center shrink-0 mr-2 text-zinc-400 group-hover/node:text-zinc-600 dark:text-zinc-400 dark:group-hover/node:text-zinc-200\">\n              {node.icon ? (\n                node.icon\n              ) : hasChildren ? (\n                isExpanded ? (\n                  <FolderOpen className=\"h-4 w-4 text-amber-500/90 dark:text-amber-400/90\" />\n                ) : (\n                  <Folder className=\"h-4 w-4 text-amber-500/90 dark:text-amber-400/90\" />\n                )\n              ) : (\n                <File className=\"h-4 w-4 text-zinc-400 dark:text-zinc-500\" />\n              )}\n            </div>\n          )}\n\n          <span className=\"text-xs sm:text-sm truncate leading-none pt-px\">\n            {node.label}\n          </span>\n        </div>\n\n        <AnimatePresence initial={false}>\n          {hasChildren && isExpanded && (\n            <motion.div\n              initial={{ height: 0, opacity: 0 }}\n              animate={{\n                height: \"auto\",\n                opacity: 1,\n                transition:\n                  prefersReducedMotion || !animateExpand\n                    ? { duration: 0 }\n                    : {\n                        height: { duration: 0.2, ease: [0.16, 1, 0.3, 1] },\n                        opacity: { duration: 0.15, delay: 0.03 },\n                      },\n              }}\n              exit={{\n                height: 0,\n                opacity: 0,\n                transition:\n                  prefersReducedMotion || !animateExpand\n                    ? { duration: 0 }\n                    : {\n                        height: { duration: 0.18, ease: [0.16, 1, 0.3, 1] },\n                        opacity: { duration: 0.1 },\n                      },\n              }}\n              role=\"group\"\n              className=\"overflow-hidden\"\n            >\n              {node.children!.map((child) => renderNode(child, level + 1))}\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </div>\n    );\n  };\n\n  return (\n    <div\n      role=\"tree\"\n      className={cn(\n        \"w-full select-none text-zinc-900 dark:text-zinc-100 font-sans\",\n        className,\n      )}\n    >\n      {data.map((node) => renderNode(node, 0))}\n    </div>\n  );\n}\n\nexport const FileTree = TreeView;\n",
      "content": "\"use client\";\n\nimport React, { useState, useCallback, useMemo, useRef } from \"react\";\nimport { ChevronRight, Folder, FolderOpen, File } from \"lucide-react\";\nimport { motion, AnimatePresence, useReducedMotion } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type TreeNode = {\n  id: string;\n  label: string;\n  icon?: React.ReactNode;\n  children?: TreeNode[];\n  data?: unknown;\n  disabled?: boolean;\n};\n\nexport type TreeViewProps = {\n  data: TreeNode[];\n  className?: string;\n  onNodeClick?: (node: TreeNode) => void;\n  onNodeExpand?: (nodeId: string, expanded: boolean) => void;\n  defaultExpandedIds?: string[];\n  expandedIds?: string[];\n  onExpandedIdsChange?: (expandedIds: string[]) => void;\n  showLines?: boolean;\n  showIcons?: boolean;\n  selectable?: boolean;\n  multiSelect?: boolean;\n  selectedIds?: string[];\n  defaultSelectedIds?: string[];\n  onSelectionChange?: (selectedIds: string[]) => void;\n  indent?: number;\n  animateExpand?: boolean;\n  expandOnFolderClick?: boolean;\n};\n\nexport function TreeView({\n  data,\n  className,\n  onNodeClick,\n  onNodeExpand,\n  defaultExpandedIds = [],\n  expandedIds: controlledExpandedIds,\n  onExpandedIdsChange,\n  showLines = true,\n  showIcons = true,\n  selectable = true,\n  multiSelect = false,\n  selectedIds: controlledSelectedIds,\n  defaultSelectedIds = [],\n  onSelectionChange,\n  indent = 16,\n  animateExpand = true,\n  expandOnFolderClick = true,\n}: TreeViewProps) {\n  const prefersReducedMotion = useReducedMotion();\n\n  const [internalExpandedIds, setInternalExpandedIds] = useState<Set<string>>(\n    () => new Set(defaultExpandedIds),\n  );\n\n  const isExpandedControlled = controlledExpandedIds !== undefined;\n  const currentExpandedSet = useMemo(() => {\n    if (isExpandedControlled) {\n      return new Set(controlledExpandedIds);\n    }\n    return internalExpandedIds;\n  }, [isExpandedControlled, controlledExpandedIds, internalExpandedIds]);\n\n  const toggleExpanded = useCallback(\n    (nodeId: string) => {\n      const nextSet = new Set(currentExpandedSet);\n      const isExpanded = nextSet.has(nodeId);\n      if (isExpanded) {\n        nextSet.delete(nodeId);\n      } else {\n        nextSet.add(nodeId);\n      }\n\n      if (!isExpandedControlled) {\n        setInternalExpandedIds(nextSet);\n      }\n\n      onExpandedIdsChange?.(Array.from(nextSet));\n      onNodeExpand?.(nodeId, !isExpanded);\n    },\n    [\n      currentExpandedSet,\n      isExpandedControlled,\n      onExpandedIdsChange,\n      onNodeExpand,\n    ],\n  );\n\n  const [internalSelectedIds, setInternalSelectedIds] =\n    useState<string[]>(defaultSelectedIds);\n\n  const isSelectedControlled = controlledSelectedIds !== undefined;\n  const currentSelectedIds = isSelectedControlled\n    ? controlledSelectedIds\n    : internalSelectedIds;\n\n  const handleSelection = useCallback(\n    (nodeId: string, isCtrlKey: boolean) => {\n      if (!selectable) return;\n\n      let nextSelection: string[];\n\n      if (multiSelect && isCtrlKey) {\n        nextSelection = currentSelectedIds.includes(nodeId)\n          ? currentSelectedIds.filter((id) => id !== nodeId)\n          : [...currentSelectedIds, nodeId];\n      } else {\n        nextSelection = currentSelectedIds.includes(nodeId) ? [] : [nodeId];\n      }\n\n      if (!isSelectedControlled) {\n        setInternalSelectedIds(nextSelection);\n      }\n\n      onSelectionChange?.(nextSelection);\n    },\n    [\n      selectable,\n      multiSelect,\n      currentSelectedIds,\n      isSelectedControlled,\n      onSelectionChange,\n    ],\n  );\n\n  const visibleNodes = useMemo(() => {\n    const list: { node: TreeNode; parentId: string | null }[] = [];\n    const walk = (nodes: TreeNode[], parentId: string | null) => {\n      for (const node of nodes) {\n        list.push({ node, parentId });\n        if (node.children?.length && currentExpandedSet.has(node.id)) {\n          walk(node.children, node.id);\n        }\n      }\n    };\n    walk(data, null);\n    return list;\n  }, [data, currentExpandedSet]);\n\n  const itemRefs = useRef(new Map<string, HTMLDivElement>());\n  const [focusedId, setFocusedId] = useState<string | null>(null);\n\n  const tabStopId = useMemo(() => {\n    const visibleIds = visibleNodes.map(({ node }) => node.id);\n    if (focusedId && visibleIds.includes(focusedId)) return focusedId;\n    return (\n      visibleIds.find((id) => currentSelectedIds.includes(id)) ??\n      visibleIds[0] ??\n      null\n    );\n  }, [visibleNodes, focusedId, currentSelectedIds]);\n\n  const focusNode = useCallback((nodeId: string | null | undefined) => {\n    if (!nodeId) return;\n    setFocusedId(nodeId);\n    itemRefs.current.get(nodeId)?.focus();\n  }, []);\n\n  const moveFocus = useCallback(\n    (nodeId: string, key: string) => {\n      const index = visibleNodes.findIndex(({ node }) => node.id === nodeId);\n      if (index === -1) return false;\n      const current = visibleNodes[index];\n      const hasChildren = (current.node.children?.length ?? 0) > 0;\n      const isExpanded = currentExpandedSet.has(nodeId);\n\n      switch (key) {\n        case \"ArrowDown\":\n          focusNode(visibleNodes[index + 1]?.node.id);\n          return true;\n        case \"ArrowUp\":\n          focusNode(visibleNodes[index - 1]?.node.id);\n          return true;\n        case \"Home\":\n          focusNode(visibleNodes[0]?.node.id);\n          return true;\n        case \"End\":\n          focusNode(visibleNodes[visibleNodes.length - 1]?.node.id);\n          return true;\n        case \"ArrowRight\":\n          if (!hasChildren) return false;\n          if (isExpanded) focusNode(current.node.children?.[0]?.id);\n          else toggleExpanded(nodeId);\n          return true;\n        case \"ArrowLeft\":\n          if (hasChildren && isExpanded) toggleExpanded(nodeId);\n          else focusNode(current.parentId);\n          return true;\n        default:\n          return false;\n      }\n    },\n    [visibleNodes, currentExpandedSet, focusNode, toggleExpanded],\n  );\n\n  const handleNodeClick = useCallback(\n    (node: TreeNode, e: React.MouseEvent) => {\n      e.stopPropagation();\n      if (node.disabled) return;\n\n      const hasChildren = (node.children?.length ?? 0) > 0;\n      if (hasChildren && expandOnFolderClick) {\n        toggleExpanded(node.id);\n      }\n\n      handleSelection(node.id, e.ctrlKey || e.metaKey);\n      onNodeClick?.(node);\n    },\n    [expandOnFolderClick, toggleExpanded, handleSelection, onNodeClick],\n  );\n\n  const renderNode = (node: TreeNode, level = 0) => {\n    const hasChildren = (node.children?.length ?? 0) > 0;\n    const isExpanded = currentExpandedSet.has(node.id);\n    const isSelected = currentSelectedIds.includes(node.id);\n\n    return (\n      <div key={node.id} className=\"relative\">\n        <div\n          ref={(element) => {\n            if (element) itemRefs.current.set(node.id, element);\n            else itemRefs.current.delete(node.id);\n          }}\n          role=\"treeitem\"\n          aria-level={level + 1}\n          aria-expanded={hasChildren ? isExpanded : undefined}\n          aria-selected={selectable ? isSelected : undefined}\n          aria-disabled={node.disabled || undefined}\n          tabIndex={node.id === tabStopId ? 0 : -1}\n          onFocus={() => setFocusedId(node.id)}\n          className={cn(\n            \"group/node relative flex items-center h-8 px-2 rounded-md cursor-pointer select-none transition-colors duration-150 outline-none focus-visible:ring-1 focus-visible:ring-zinc-400 dark:focus-visible:ring-zinc-600\",\n            isSelected\n              ? \"bg-zinc-100 dark:bg-zinc-800/80 text-zinc-900 dark:text-zinc-100 font-medium\"\n              : \"text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100/70 dark:hover:bg-zinc-800/50 hover:text-zinc-900 dark:hover:text-zinc-100\",\n            node.disabled && \"opacity-50 pointer-events-none\",\n          )}\n          style={{ paddingLeft: `${level * indent + 8}px` }}\n          onClick={(e) => handleNodeClick(node, e)}\n          onKeyDown={(e) => {\n            if (e.key === \"Enter\" || e.key === \" \") {\n              e.preventDefault();\n              handleNodeClick(node, e as unknown as React.MouseEvent);\n            } else if (moveFocus(node.id, e.key)) {\n              e.preventDefault();\n            }\n          }}\n        >\n          {showLines && level > 0 && (\n            <div className=\"absolute left-0 top-0 bottom-0 pointer-events-none\">\n              {Array.from({ length: level }).map((_, i) => (\n                <div\n                  key={i}\n                  className=\"absolute top-0 bottom-0 border-l border-zinc-200 dark:border-zinc-800/80\"\n                  style={{ left: `${i * indent + 15}px` }}\n                />\n              ))}\n            </div>\n          )}\n\n          <div className=\"w-4 h-4 flex items-center justify-center shrink-0 mr-1\">\n            {hasChildren ? (\n              <motion.div\n                animate={{ rotate: isExpanded ? 90 : 0 }}\n                transition={\n                  prefersReducedMotion || !animateExpand\n                    ? { duration: 0 }\n                    : { duration: 0.15, ease: \"easeInOut\" }\n                }\n                className=\"flex items-center justify-center text-zinc-400 group-hover/node:text-zinc-600 dark:group-hover/node:text-zinc-300\"\n                onClick={(e) => {\n                  if (!expandOnFolderClick) {\n                    e.stopPropagation();\n                    toggleExpanded(node.id);\n                  }\n                }}\n              >\n                <ChevronRight className=\"h-3.5 w-3.5 shrink-0\" />\n              </motion.div>\n            ) : (\n              <span className=\"w-3.5 h-3.5\" />\n            )}\n          </div>\n\n          {showIcons && (\n            <div className=\"w-4 h-4 flex items-center justify-center shrink-0 mr-2 text-zinc-400 group-hover/node:text-zinc-600 dark:text-zinc-400 dark:group-hover/node:text-zinc-200\">\n              {node.icon ? (\n                node.icon\n              ) : hasChildren ? (\n                isExpanded ? (\n                  <FolderOpen className=\"h-4 w-4 text-amber-500/90 dark:text-amber-400/90\" />\n                ) : (\n                  <Folder className=\"h-4 w-4 text-amber-500/90 dark:text-amber-400/90\" />\n                )\n              ) : (\n                <File className=\"h-4 w-4 text-zinc-400 dark:text-zinc-500\" />\n              )}\n            </div>\n          )}\n\n          <span className=\"text-xs sm:text-sm truncate leading-none pt-px\">\n            {node.label}\n          </span>\n        </div>\n\n        <AnimatePresence initial={false}>\n          {hasChildren && isExpanded && (\n            <motion.div\n              initial={{ height: 0, opacity: 0 }}\n              animate={{\n                height: \"auto\",\n                opacity: 1,\n                transition:\n                  prefersReducedMotion || !animateExpand\n                    ? { duration: 0 }\n                    : {\n                        height: { duration: 0.2, ease: [0.16, 1, 0.3, 1] },\n                        opacity: { duration: 0.15, delay: 0.03 },\n                      },\n              }}\n              exit={{\n                height: 0,\n                opacity: 0,\n                transition:\n                  prefersReducedMotion || !animateExpand\n                    ? { duration: 0 }\n                    : {\n                        height: { duration: 0.18, ease: [0.16, 1, 0.3, 1] },\n                        opacity: { duration: 0.1 },\n                      },\n              }}\n              role=\"group\"\n              className=\"overflow-hidden\"\n            >\n              {node.children!.map((child) => renderNode(child, level + 1))}\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </div>\n    );\n  };\n\n  return (\n    <div\n      role=\"tree\"\n      className={cn(\n        \"w-full select-none text-zinc-900 dark:text-zinc-100 font-sans\",\n        className,\n      )}\n    >\n      {data.map((node) => renderNode(node, 0))}\n    </div>\n  );\n}\n\nexport const FileTree = TreeView;\n",
      "type": "registry:ui",
      "target": "@ui/file-tree.tsx"
    }
  ],
  "type": "registry:ui",
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "title": "File Tree"
}
