{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "file-tree",
  "title": "File Tree",
  "description": "Animated file-tree component with animated hover highlight, spring folder expand/collapse, and automatic file-type icons.",
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "files": [
    {
      "path": "registry/components/unlumen/file-tree/index.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n  Folder,\n  FolderOpen,\n  File,\n  FileText,\n  FileCode,\n  FileJson,\n  FileImage,\n  FileCog,\n} from \"lucide-react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\n// ─── Types ─────────────────────────────────────────────────────────────────────\n\nexport type FileTreeElement = {\n  id: string;\n  name: string;\n  /** Omit or set to \"file\" for a leaf node; \"folder\" renders a collapsible branch. */\n  type?: \"folder\" | \"file\";\n  children?: FileTreeElement[];\n  /** Custom icon component (receives a `className` prop). */\n  icon?: React.ComponentType<{ className?: string }>;\n  /** Pink-tints the item to mark it as newly added / relevant. */\n  highlight?: boolean;\n  /** Whether this folder starts expanded. */\n  defaultOpen?: boolean;\n};\n\n// ─── Context ───────────────────────────────────────────────────────────────────\n\ntype FileTreeCtx = {\n  highlightColor: string;\n  indentSize: number;\n  showIcons: boolean;\n  defaultOpenIds: Set<string>;\n  containerRef: React.RefObject<HTMLDivElement | null>;\n  highlightBounds: HighlightBounds | null;\n  setHighlightBounds: React.Dispatch<\n    React.SetStateAction<HighlightBounds | null>\n  >;\n};\n\ntype HighlightBounds = {\n  top: number;\n  left: number;\n  width: number;\n  height: number;\n};\n\nconst FileTreeContext = React.createContext<FileTreeCtx | null>(null);\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\ntype FolderCtx = {\n  isOpen: boolean;\n  toggle: () => void;\n};\n\nconst FolderContext = React.createContext<FolderCtx | null>(null);\n\nfunction useFolder() {\n  const context = React.useContext(FolderContext);\n  if (!context) {\n    throw new Error(\"Folder components must be used within a folder item\");\n  }\n  return context;\n}\n\n// ─── Icon resolution ───────────────────────────────────────────────────────────\n\nconst EXT_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {\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  config: FileCog,\n  toml: FileCog,\n  yaml: FileCog,\n  yml: FileCog,\n  env: FileCog,\n};\n\nfunction resolveFileIcon(\n  name: string,\n  custom?: React.ComponentType<{ className?: string }>,\n): React.ComponentType<{ className?: string }> {\n  if (custom) return custom;\n  const ext = name.split(\".\").pop()?.toLowerCase() ?? \"\";\n  return EXT_ICONS[ext] ?? File;\n}\n\n// ─── Shared highlight/collapse pieces ──────────────────────────────────────────\n\nfunction FileTreeHoverHighlight({ className }: { className?: string }) {\n  const { highlightBounds } = useFileTree();\n\n  return (\n    <AnimatePresence>\n      {highlightBounds && (\n        <motion.div\n          className={className}\n          initial={{ opacity: 0 }}\n          animate={{\n            opacity: 1,\n            top: highlightBounds.top,\n            left: highlightBounds.left,\n            width: highlightBounds.width,\n            height: highlightBounds.height,\n          }}\n          exit={{ opacity: 0 }}\n          transition={{ type: \"spring\", stiffness: 500, damping: 40 }}\n          style={{ position: \"absolute\", pointerEvents: \"none\", zIndex: 0 }}\n        />\n      )}\n    </AnimatePresence>\n  );\n}\n\nfunction useHighlightTarget() {\n  const { containerRef, setHighlightBounds } = useFileTree();\n  const ref = React.useRef<HTMLDivElement>(null);\n\n  const onMouseEnter = React.useCallback(() => {\n    const element = ref.current;\n    const container = containerRef.current;\n    if (!element || !container) return;\n\n    const containerRect = container.getBoundingClientRect();\n    const elementRect = element.getBoundingClientRect();\n\n    setHighlightBounds({\n      top: elementRect.top - containerRect.top,\n      left: elementRect.left - containerRect.left,\n      width: elementRect.width,\n      height: elementRect.height,\n    });\n  }, [containerRef, setHighlightBounds]);\n\n  return { ref, onMouseEnter };\n}\n\nfunction FolderIcon({\n  closeIcon,\n  openIcon,\n}: {\n  closeIcon: React.ReactNode;\n  openIcon: React.ReactNode;\n}) {\n  const { isOpen } = useFolder();\n\n  return (\n    <span className=\"inline-flex shrink-0 relative size-[1.125rem]\">\n      <AnimatePresence initial={false} mode=\"popLayout\">\n        <motion.span\n          key={isOpen ? \"open\" : \"close\"}\n          className=\"inline-flex\"\n          initial={{ scale: 0.5, opacity: 0, rotate: -15 }}\n          animate={{ scale: 1, opacity: 1, rotate: 0 }}\n          exit={{ scale: 0.5, opacity: 0, rotate: 15 }}\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({ children }: { children: React.ReactNode }) {\n  const { isOpen } = useFolder();\n\n  return (\n    <AnimatePresence initial={false}>\n      {isOpen && (\n        <motion.div\n          initial={{ height: 0, opacity: 0 }}\n          animate={{ height: \"auto\", opacity: 1 }}\n          exit={{ height: 0, opacity: 0 }}\n          transition={{ type: \"spring\", stiffness: 500, damping: 40 }}\n          style={{ overflow: \"hidden\" }}\n        >\n          {children}\n        </motion.div>\n      )}\n    </AnimatePresence>\n  );\n}\n\n// ─── Node renderers ────────────────────────────────────────────────────────────\n\nfunction FileTreeFile({ node }: { node: FileTreeElement }) {\n  const { highlightColor, showIcons } = useFileTree();\n  const Icon = resolveFileIcon(node.name, node.icon);\n  const highlightTarget = useHighlightTarget();\n\n  return (\n    <div\n      ref={highlightTarget.ref}\n      className=\"relative z-10\"\n      onMouseEnter={highlightTarget.onMouseEnter}\n    >\n      <div\n        className=\"flex items-center gap-2 p-2 pointer-events-none\"\n        style={node.highlight ? { color: highlightColor } : undefined}\n      >\n        {showIcons && (\n          <span className=\"inline-flex shrink-0\">\n            <Icon className=\"size-4.5\" />\n          </span>\n        )}\n        <span className=\"text-sm\">{node.name}</span>\n      </div>\n    </div>\n  );\n}\n\nfunction FileTreeFolder({ node }: { node: FileTreeElement }) {\n  const { defaultOpenIds, highlightColor, indentSize, showIcons } =\n    useFileTree();\n  const highlightTarget = useHighlightTarget();\n  const [isOpen, setIsOpen] = React.useState(\n    node.defaultOpen ?? defaultOpenIds.has(node.id),\n  );\n  const toggle = React.useCallback(() => setIsOpen((open) => !open), []);\n\n  return (\n    <FolderContext.Provider value={{ isOpen, toggle }}>\n      <div data-value={node.id} className=\"relative z-10\">\n        <button type=\"button\" className=\"w-full text-start\" onClick={toggle}>\n          <div\n            ref={highlightTarget.ref}\n            onMouseEnter={highlightTarget.onMouseEnter}\n          >\n            <div className=\"flex items-center gap-2 p-2 pointer-events-none\">\n              {showIcons && (\n                <FolderIcon\n                  closeIcon={<Folder className=\"size-4.5\" />}\n                  openIcon={<FolderOpen className=\"size-4.5\" />}\n                />\n              )}\n              <span\n                className=\"text-sm\"\n                style={node.highlight ? { color: highlightColor } : undefined}\n              >\n                {node.name}\n              </span>\n            </div>\n          </div>\n        </button>\n        <div\n          className=\"relative ml-6 before:absolute before:-left-2 before:inset-y-0 before:w-px before:h-full before:bg-border\"\n          style={indentSize !== 24 ? { marginLeft: indentSize } : undefined}\n        >\n          <FolderContent>\n            {(node.children ?? []).map((child) => (\n              <FileTreeNode key={child.id} node={child} />\n            ))}\n          </FolderContent>\n        </div>\n      </div>\n    </FolderContext.Provider>\n  );\n}\n\nfunction FileTreeNode({ node }: { node: FileTreeElement }) {\n  if (node.type === \"folder\") {\n    return <FileTreeFolder node={node} />;\n  }\n  return <FileTreeFile node={node} />;\n}\n\n// ─── Public API ────────────────────────────────────────────────────────────────\n\nexport type FileTreeProps = {\n  elements: FileTreeElement[];\n  className?: string;\n  /** Highlight color for items with `highlight: true`. Defaults to pink (#f472b6). */\n  highlightColor?: string;\n  /** Horizontal indent per nesting level in px. Defaults to 24. */\n  indentSize?: number;\n  /** Whether to show file/folder icons. Defaults to true. */\n  showIcons?: boolean;\n  /** Folder ids that should be open on first render. */\n  defaultOpenIds?: string[];\n};\n\nexport function FileTree({\n  elements,\n  className,\n  highlightColor = \"#f472b6\",\n  indentSize = 24,\n  showIcons = true,\n  defaultOpenIds = [],\n}: FileTreeProps) {\n  const containerRef = React.useRef<HTMLDivElement>(null);\n  const [highlightBounds, setHighlightBounds] =\n    React.useState<HighlightBounds | null>(null);\n  const defaultOpenIdSet = React.useMemo(\n    () => new Set(defaultOpenIds),\n    [defaultOpenIds],\n  );\n\n  return (\n    <FileTreeContext.Provider\n      value={{\n        highlightColor,\n        indentSize,\n        showIcons,\n        defaultOpenIds: defaultOpenIdSet,\n        containerRef,\n        highlightBounds,\n        setHighlightBounds,\n      }}\n    >\n      <div\n        className={cn(\n          \"rounded-xl border border-border/60 overflow-hidden\",\n          className,\n        )}\n      >\n        <div\n          ref={containerRef}\n          className=\"p-2 w-full relative isolate\"\n          onMouseLeave={() => setHighlightBounds(null)}\n        >\n          <FileTreeHoverHighlight className=\"rounded-lg border bg-accent/55 border-accent/45 z-0\" />\n          {elements.map((node) => (\n            <FileTreeNode key={node.id} node={node} />\n          ))}\n        </div>\n      </div>\n    </FileTreeContext.Provider>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/unlumen-ui/file-tree.tsx"
    }
  ],
  "type": "registry:component"
}