import React, {
createContext,
useCallback,
useContext,
useMemo,
useState,
type ReactNode,
} from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import {
ChevronRight,
File as FileIcon,
Folder,
FolderOpen,
} from 'lucide-react-native';
import Animated, {
useAnimatedStyle,
withTiming,
useSharedValue,
} from 'react-native-reanimated';
// ====================== Custom Collapsible ======================
type CollapsibleProps = {
open: boolean;
children: ReactNode;
};
const Collapsible = ({ open, children }: CollapsibleProps) => {
const progress = useSharedValue(open ? 1 : 0);
const animatedStyle = useAnimatedStyle(() => ({
opacity: progress.value,
maxHeight: progress.value * 9999,
}),[progress.value]);
React.useEffect(() => {
progress.value = withTiming(open ? 1 : 0, { duration: 180 });
}, [open]);
return {children};
};
// ====================== Contexts ======================
interface FileTreeContextType {
expandedPaths: Set;
togglePath: (path: string) => void;
selectedPath?: string;
onSelect?: (path: string) => void;
}
const FileTreeContext = createContext({
expandedPaths: new Set(),
togglePath: () => {},
});
interface FileTreeFolderContextType {
path: string;
name: string;
isExpanded: boolean;
}
const FileTreeFolderContext = createContext({
path: '',
name: '',
isExpanded: false,
});
// ====================== Main FileTree ======================
export type FileTreeProps = {
expanded?: Set;
defaultExpanded?: Set;
selectedPath?: string;
onSelect?: (path: string) => void;
onExpandedChange?: (expanded: Set) => void;
className?: string;
children: ReactNode;
};
export const FileTree = ({
expanded: controlledExpanded,
defaultExpanded = new Set(),
selectedPath,
onSelect,
onExpandedChange,
className,
children,
}: 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, togglePath, selectedPath, onSelect }),
[expandedPaths, togglePath, selectedPath, onSelect]
);
return (
{children}
);
};
// ====================== Folder ======================
export type FileTreeFolderProps = {
path: string;
name: string;
children?: ReactNode;
className?: string;
};
export const FileTreeFolder = ({
path,
name,
children,
className,
}: FileTreeFolderProps) => {
const { expandedPaths, togglePath, selectedPath, onSelect } =
useContext(FileTreeContext);
const isExpanded = expandedPaths.has(path);
const isSelected = selectedPath === path;
const handleToggle = useCallback(() => togglePath(path), [togglePath, path]);
const handleSelect = useCallback(() => onSelect?.(path), [onSelect, path]);
const folderContextValue = useMemo(
() => ({ isExpanded, name, path }),
[isExpanded, name, path]
);
return (
{isExpanded ? (
) : (
)}
{name}
{children}
);
};
// ====================== File ======================
export type FileTreeFileProps = {
path: string;
name: string;
icon?: ReactNode;
className?: string;
};
export const FileTreeFile = ({
path,
name,
icon,
className,
}: FileTreeFileProps) => {
const { selectedPath, onSelect } = useContext(FileTreeContext);
const isSelected = selectedPath === path;
const handlePress = useCallback(() => onSelect?.(path), [onSelect, path]);
return (
{icon ?? }
{name}
);
};
// ====================== Actions ======================
export const FileTreeActions = ({
children,
className,
}: {
children: ReactNode;
className?: string;
}) => {
return (
{children}
);
};