{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "file-tree",
  "type": "registry:ui",
  "title": "File Tree",
  "description": "A component used to showcase the folder and file structure of a directory.",
  "files": [
    {
      "path": "registry/magicui/file-tree.tsx",
      "content": "\"use client\"\n\nimport React, {\n  createContext,\n  forwardRef,\n  useCallback,\n  useContext,\n  useEffect,\n  useState,\n} from \"react\"\nimport * as AccordionPrimitive from \"@radix-ui/react-accordion\"\nimport { FileIcon, FolderIcon, FolderOpenIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport { ScrollArea } from \"@/components/ui/scroll-area\"\n\ntype TreeViewElement = {\n  id: string\n  name: string\n  type?: \"file\" | \"folder\"\n  isSelectable?: boolean\n  children?: TreeViewElement[]\n}\n\ntype TreeSortMode =\n  | \"default\"\n  | \"none\"\n  | ((a: TreeViewElement, b: TreeViewElement) => number)\n\ntype TreeContextProps = {\n  selectedId: string | undefined\n  expandedItems: string[] | undefined\n  indicator: boolean\n  handleExpand: (id: string) => void\n  selectItem: (id: string) => void\n  setExpandedItems?: React.Dispatch<React.SetStateAction<string[] | undefined>>\n  openIcon?: React.ReactNode\n  closeIcon?: React.ReactNode\n  direction: \"rtl\" | \"ltr\"\n}\n\nconst TreeContext = createContext<TreeContextProps | null>(null)\n\nconst useTree = () => {\n  const context = useContext(TreeContext)\n  if (!context) {\n    throw new Error(\"useTree must be used within a TreeProvider\")\n  }\n  return context\n}\n\ntype Direction = \"rtl\" | \"ltr\" | undefined\n\nconst isFolderElement = (element: TreeViewElement) => {\n  if (element.type) {\n    return element.type === \"folder\"\n  }\n\n  return Array.isArray(element.children)\n}\n\nconst mergeExpandedItems = (\n  currentItems: string[] | undefined,\n  nextItems: string[]\n) => [...new Set([...(currentItems ?? []), ...nextItems])]\n\nconst treeCollator = new Intl.Collator(\"en\", {\n  numeric: true,\n  sensitivity: \"base\",\n})\n\nconst defaultTreeComparator = (a: TreeViewElement, b: TreeViewElement) => {\n  const aIsFolder = isFolderElement(a)\n  const bIsFolder = isFolderElement(b)\n\n  if (aIsFolder !== bIsFolder) {\n    return aIsFolder ? -1 : 1\n  }\n\n  return treeCollator.compare(a.name, b.name)\n}\n\nconst getTreeComparator = (sort: TreeSortMode) => {\n  if (sort === \"none\") {\n    return undefined\n  }\n\n  if (sort === \"default\") {\n    return defaultTreeComparator\n  }\n\n  return sort\n}\n\nconst sortTreeElements = (\n  elements: TreeViewElement[],\n  sort: TreeSortMode\n): TreeViewElement[] => {\n  const comparator = getTreeComparator(sort)\n\n  const nextElements = elements.map((element) => {\n    if (!Array.isArray(element.children)) {\n      return element\n    }\n\n    return {\n      ...element,\n      children: sortTreeElements(element.children, sort),\n    }\n  })\n\n  if (!comparator) {\n    return nextElements\n  }\n\n  return [...nextElements].sort(comparator)\n}\n\nconst renderTreeElements = (\n  elements: TreeViewElement[],\n  sort: TreeSortMode\n): React.ReactNode =>\n  sortTreeElements(elements, sort).map((element) => {\n    if (isFolderElement(element)) {\n      return (\n        <Folder\n          key={element.id}\n          value={element.id}\n          element={element.name}\n          isSelectable={element.isSelectable}\n        >\n          {Array.isArray(element.children)\n            ? renderTreeElements(element.children, sort)\n            : null}\n        </Folder>\n      )\n    }\n\n    return (\n      <File\n        key={element.id}\n        value={element.id}\n        isSelectable={element.isSelectable}\n      >\n        <span>{element.name}</span>\n      </File>\n    )\n  })\n\ntype TreeViewProps = {\n  initialSelectedId?: string\n  indicator?: boolean\n  elements?: TreeViewElement[]\n  initialExpandedItems?: string[]\n  openIcon?: React.ReactNode\n  closeIcon?: React.ReactNode\n  sort?: TreeSortMode\n} & Omit<\n  React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Root>,\n  \"defaultValue\" | \"onValueChange\" | \"type\" | \"value\"\n>\n\nconst Tree = forwardRef<HTMLDivElement, TreeViewProps>(\n  (\n    {\n      className,\n      elements,\n      initialSelectedId,\n      initialExpandedItems,\n      children,\n      indicator = true,\n      openIcon,\n      closeIcon,\n      sort = \"default\",\n      dir,\n      ...props\n    },\n    ref\n  ) => {\n    const [selectedId, setSelectedId] = useState<string | undefined>(\n      initialSelectedId\n    )\n    const [expandedItems, setExpandedItems] = useState<string[] | undefined>(\n      initialExpandedItems\n    )\n\n    const selectItem = useCallback((id: string) => {\n      setSelectedId(id)\n    }, [])\n\n    const handleExpand = useCallback((id: string) => {\n      setExpandedItems((prev) => {\n        if (prev?.includes(id)) {\n          return prev.filter((item) => item !== id)\n        }\n        return [...(prev ?? []), id]\n      })\n    }, [])\n\n    const expandSpecificTargetedElements = useCallback(\n      (elements?: TreeViewElement[], selectId?: string) => {\n        if (!elements || !selectId) return\n        const findParent = (\n          currentElement: TreeViewElement,\n          currentPath: string[] = []\n        ) => {\n          const isSelectable = currentElement.isSelectable ?? true\n          const newPath = [...currentPath, currentElement.id]\n          if (currentElement.id === selectId) {\n            if (isSelectable) {\n              setExpandedItems((prev) => mergeExpandedItems(prev, newPath))\n            } else {\n              if (newPath.includes(currentElement.id)) {\n                newPath.pop()\n                setExpandedItems((prev) => mergeExpandedItems(prev, newPath))\n              }\n            }\n            return\n          }\n          if (\n            Array.isArray(currentElement.children) &&\n            currentElement.children.length > 0\n          ) {\n            currentElement.children.forEach((child) => {\n              findParent(child, newPath)\n            })\n          }\n        }\n        elements.forEach((element) => {\n          findParent(element)\n        })\n      },\n      []\n    )\n\n    useEffect(() => {\n      if (initialSelectedId) {\n        expandSpecificTargetedElements(elements, initialSelectedId)\n      }\n    }, [initialSelectedId, elements, expandSpecificTargetedElements])\n\n    const direction = dir === \"rtl\" ? \"rtl\" : \"ltr\"\n    const treeChildren =\n      children ?? (elements ? renderTreeElements(elements, sort) : null)\n\n    return (\n      <TreeContext.Provider\n        value={{\n          selectedId,\n          expandedItems,\n          handleExpand,\n          selectItem,\n          setExpandedItems,\n          indicator,\n          openIcon,\n          closeIcon,\n          direction,\n        }}\n      >\n        <div className={cn(\"size-full\", className)}>\n          <ScrollArea\n            ref={ref}\n            className=\"relative h-full px-2\"\n            dir={dir as Direction}\n          >\n            <AccordionPrimitive.Root\n              {...props}\n              type=\"multiple\"\n              value={expandedItems}\n              className=\"flex flex-col gap-1\"\n              dir={dir as Direction}\n            >\n              {treeChildren}\n            </AccordionPrimitive.Root>\n          </ScrollArea>\n        </div>\n      </TreeContext.Provider>\n    )\n  }\n)\n\nTree.displayName = \"Tree\"\n\nconst TreeIndicator = forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => {\n  const { direction } = useTree()\n\n  return (\n    <div\n      dir={direction}\n      ref={ref}\n      className={cn(\n        \"bg-muted absolute left-1.5 h-full w-px rounded-md py-3 duration-300 ease-in-out hover:bg-slate-300 rtl:right-1.5\",\n        className\n      )}\n      {...props}\n    />\n  )\n})\n\nTreeIndicator.displayName = \"TreeIndicator\"\n\ntype FolderProps = {\n  expandedItems?: string[]\n  element: string\n  isSelectable?: boolean\n  isSelect?: boolean\n} & React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>\n\nconst Folder = forwardRef<\n  HTMLDivElement,\n  FolderProps & React.HTMLAttributes<HTMLDivElement>\n>(\n  (\n    {\n      className,\n      element,\n      value,\n      isSelectable = true,\n      isSelect,\n      children,\n      ...props\n    },\n    ref\n  ) => {\n    const {\n      direction,\n      handleExpand,\n      expandedItems,\n      indicator,\n      selectedId,\n      selectItem,\n      openIcon,\n      closeIcon,\n    } = useTree()\n    const isSelected = isSelect ?? selectedId === value\n\n    return (\n      <AccordionPrimitive.Item\n        ref={ref}\n        {...props}\n        value={value}\n        className=\"relative h-full overflow-hidden\"\n      >\n        <AccordionPrimitive.Trigger\n          className={cn(\n            `flex items-center gap-1 rounded-md text-sm`,\n            className,\n            {\n              \"bg-muted rounded-md\": isSelected && isSelectable,\n              \"cursor-pointer\": isSelectable,\n              \"cursor-not-allowed opacity-50\": !isSelectable,\n            }\n          )}\n          disabled={!isSelectable}\n          onClick={() => {\n            selectItem(value)\n            handleExpand(value)\n          }}\n        >\n          {expandedItems?.includes(value)\n            ? (openIcon ?? <FolderOpenIcon className=\"size-4\" />)\n            : (closeIcon ?? <FolderIcon className=\"size-4\" />)}\n          <span>{element}</span>\n        </AccordionPrimitive.Trigger>\n        <AccordionPrimitive.Content className=\"data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down relative h-full overflow-hidden text-sm\">\n          {element && indicator && <TreeIndicator aria-hidden=\"true\" />}\n          <AccordionPrimitive.Root\n            dir={direction}\n            type=\"multiple\"\n            className=\"ml-5 flex flex-col gap-1 py-1 rtl:mr-5\"\n            value={expandedItems}\n          >\n            {children}\n          </AccordionPrimitive.Root>\n        </AccordionPrimitive.Content>\n      </AccordionPrimitive.Item>\n    )\n  }\n)\n\nFolder.displayName = \"Folder\"\n\nconst File = forwardRef<\n  HTMLButtonElement,\n  {\n    value: string\n    handleSelect?: (id: string) => void\n    isSelectable?: boolean\n    isSelect?: boolean\n    fileIcon?: React.ReactNode\n  } & React.ButtonHTMLAttributes<HTMLButtonElement>\n>(\n  (\n    {\n      value,\n      className,\n      handleSelect,\n      onClick,\n      isSelectable = true,\n      isSelect,\n      fileIcon,\n      children,\n      ...props\n    },\n    ref\n  ) => {\n    const { direction, selectedId, selectItem } = useTree()\n    const isSelected = isSelect ?? selectedId === value\n    return (\n      <button\n        ref={ref}\n        type=\"button\"\n        disabled={!isSelectable}\n        className={cn(\n          \"flex w-fit items-center gap-1 rounded-md pr-1 text-sm duration-200 ease-in-out rtl:pr-0 rtl:pl-1\",\n          {\n            \"bg-muted\": isSelected && isSelectable,\n          },\n          isSelectable ? \"cursor-pointer\" : \"cursor-not-allowed opacity-50\",\n          direction === \"rtl\" ? \"rtl\" : \"ltr\",\n          className\n        )}\n        onClick={(event) => {\n          selectItem(value)\n          handleSelect?.(value)\n          onClick?.(event)\n        }}\n        {...props}\n      >\n        {fileIcon ?? <FileIcon className=\"size-4\" />}\n        {children}\n      </button>\n    )\n  }\n)\n\nFile.displayName = \"File\"\n\nconst CollapseButton = forwardRef<\n  HTMLButtonElement,\n  {\n    elements: TreeViewElement[]\n    expandAll?: boolean\n  } & React.HTMLAttributes<HTMLButtonElement>\n>(({ className, elements, expandAll = false, children, ...props }, ref) => {\n  const { expandedItems, setExpandedItems } = useTree()\n\n  const expendAllTree = useCallback((elements: TreeViewElement[]) => {\n    const expandedElementIds: string[] = []\n\n    const expandTree = (element: TreeViewElement) => {\n      const isSelectable = element.isSelectable ?? true\n      if (isSelectable && element.children && element.children.length > 0) {\n        expandedElementIds.push(element.id)\n        for (const child of element.children) {\n          expandTree(child)\n        }\n      }\n    }\n\n    for (const element of elements) {\n      expandTree(element)\n    }\n\n    return [...new Set(expandedElementIds)]\n  }, [])\n\n  const closeAll = useCallback(() => {\n    setExpandedItems?.([])\n  }, [setExpandedItems])\n\n  useEffect(() => {\n    if (expandAll) {\n      setExpandedItems?.(expendAllTree(elements))\n    }\n  }, [expandAll, elements, expendAllTree, setExpandedItems])\n\n  return (\n    <Button\n      variant={\"ghost\"}\n      className={cn(\"absolute right-2 bottom-1 h-8 w-fit p-1\", className)}\n      onClick={\n        expandedItems && expandedItems.length > 0\n          ? closeAll\n          : () => setExpandedItems?.(expendAllTree(elements))\n      }\n      ref={ref}\n      {...props}\n    >\n      {children}\n      <span className=\"sr-only\">Toggle</span>\n    </Button>\n  )\n})\n\nCollapseButton.displayName = \"CollapseButton\"\n\nexport { CollapseButton, File, Folder, Tree, type TreeViewElement }\nexport type { TreeSortMode }\n",
      "type": "registry:ui"
    }
  ]
}