"use client"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; import { cn } from "@/lib/utils"; import { ChevronRightIcon, FileIcon, FolderIcon, FolderOpenIcon, } from "lucide-react"; import type { HTMLAttributes, ReactNode } from "react"; import { createContext, useCallback, useContext, useMemo, useState, } from "react"; interface FileTreeContextType { expandedPaths: Set; togglePath: (path: string) => void; selectedPath?: string; onSelect?: (path: string) => void; } // Default noop for context default value // oxlint-disable-next-line eslint(no-empty-function) const noop = () => {}; const FileTreeContext = createContext({ // oxlint-disable-next-line eslint-plugin-unicorn(no-new-builtin) expandedPaths: new Set(), togglePath: noop, }); export type FileTreeProps = Omit, "onSelect"> & { expanded?: Set; defaultExpanded?: Set; selectedPath?: string; onSelect?: (path: string) => void; onExpandedChange?: (expanded: Set) => void; }; export const FileTree = ({ expanded: controlledExpanded, defaultExpanded = new Set(), selectedPath, onSelect, onExpandedChange, className, children, ...props }: FileTreeProps) => { const [internalExpanded, setInternalExpanded] = useState(defaultExpanded); const expandedPaths = controlledExpanded ?? internalExpanded; const togglePath = useCallback( (path: string) => { const newExpanded = new Set(expandedPaths); if (newExpanded.has(path)) { newExpanded.delete(path); } else { newExpanded.add(path); } setInternalExpanded(newExpanded); onExpandedChange?.(newExpanded); }, [expandedPaths, onExpandedChange] ); const contextValue = useMemo( () => ({ expandedPaths, onSelect, selectedPath, togglePath }), [expandedPaths, onSelect, selectedPath, togglePath] ); return (
{children}
); }; export type FileTreeIconProps = HTMLAttributes; export const FileTreeIcon = ({ className, children, ...props }: FileTreeIconProps) => ( {children} ); export type FileTreeNameProps = HTMLAttributes; export const FileTreeName = ({ className, children, ...props }: FileTreeNameProps) => ( {children} ); interface FileTreeFolderContextType { path: string; name: string; isExpanded: boolean; } const FileTreeFolderContext = createContext({ isExpanded: false, name: "", path: "", }); export type FileTreeFolderProps = HTMLAttributes & { path: string; name: string; }; export const FileTreeFolder = ({ path, name, className, children, ...props }: FileTreeFolderProps) => { const { expandedPaths, togglePath, selectedPath, onSelect } = useContext(FileTreeContext); const isExpanded = expandedPaths.has(path); const isSelected = selectedPath === path; const handleOpenChange = useCallback(() => { togglePath(path); }, [togglePath, path]); const handleSelect = useCallback(() => { onSelect?.(path); }, [onSelect, path]); const folderContextValue = useMemo( () => ({ isExpanded, name, path }), [isExpanded, name, path] ); return (
{children}
); }; interface FileTreeFileContextType { path: string; name: string; } const FileTreeFileContext = createContext({ name: "", path: "", }); export type FileTreeFileProps = HTMLAttributes & { path: string; name: string; icon?: ReactNode; }; export const FileTreeFile = ({ path, name, icon, className, children, ...props }: FileTreeFileProps) => { const { selectedPath, onSelect } = useContext(FileTreeContext); const isSelected = selectedPath === path; const handleClick = useCallback(() => { onSelect?.(path); }, [onSelect, path]); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === "Enter" || e.key === " ") { onSelect?.(path); } }, [onSelect, path] ); const fileContextValue = useMemo(() => ({ name, path }), [name, path]); return (
{children ?? ( <> {/* Spacer for alignment */} {icon ?? } {name} )}
); }; export type FileTreeActionsProps = HTMLAttributes; const stopPropagation = (e: React.SyntheticEvent) => e.stopPropagation(); export const FileTreeActions = ({ className, children, ...props }: FileTreeActionsProps) => (
{children}
);