192 lines
5.1 KiB
TypeScript
192 lines
5.1 KiB
TypeScript
'use client';
|
|
|
|
import * as React from 'react';
|
|
import { AnimatePresence, type HTMLMotionProps, motion } from 'motion/react';
|
|
import { FileIcon, FolderIcon, FolderOpenIcon } from 'lucide-react';
|
|
|
|
import { cn } from '@workspace/ui/lib/utils';
|
|
import {
|
|
Accordion,
|
|
AccordionContent,
|
|
AccordionItem,
|
|
AccordionItemProps,
|
|
AccordionTrigger,
|
|
AccordionTriggerProps,
|
|
useAccordionItem,
|
|
} from '@/registry/radix/accordion';
|
|
import {
|
|
MotionHighlight,
|
|
MotionHighlightItem,
|
|
} from '@/registry/effects/motion-highlight';
|
|
|
|
interface FileButtonProps extends HTMLMotionProps<'div'> {
|
|
icons?: {
|
|
close: React.ReactNode;
|
|
open: React.ReactNode;
|
|
};
|
|
icon?: React.ReactNode;
|
|
open?: boolean;
|
|
layoutId?: string;
|
|
}
|
|
|
|
const FileButton = React.forwardRef<HTMLDivElement, FileButtonProps>(
|
|
({ children, icons, icon, open, layoutId, ...props }, ref) => {
|
|
return (
|
|
<MotionHighlightItem className="size-full">
|
|
<motion.div
|
|
ref={ref}
|
|
className="flex [&_svg]:size-4 items-center gap-2 p-2 h-10 relative z-10 rounded-lg w-full cursor-default"
|
|
{...props}
|
|
layoutId={layoutId}
|
|
>
|
|
{icon
|
|
? typeof icon !== 'string'
|
|
? icon
|
|
: null
|
|
: icons && (
|
|
<AnimatePresence mode="wait">
|
|
<motion.span
|
|
key={open ? 'open' : 'close'}
|
|
initial={{ scale: 0.9 }}
|
|
animate={{ scale: 1 }}
|
|
exit={{ scale: 0.9 }}
|
|
transition={{ duration: 0.15 }}
|
|
>
|
|
{open
|
|
? typeof icons.open !== 'string'
|
|
? icons.open
|
|
: null
|
|
: typeof icons.close !== 'string'
|
|
? icons.close
|
|
: null}
|
|
</motion.span>
|
|
</AnimatePresence>
|
|
)}
|
|
<motion.span className="text-sm block truncate">
|
|
{children}
|
|
</motion.span>
|
|
</motion.div>
|
|
</MotionHighlightItem>
|
|
);
|
|
},
|
|
);
|
|
FileButton.displayName = 'FileButton';
|
|
|
|
type FilesProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
defaultOpen?: string[];
|
|
open?: string[];
|
|
onOpenChange?: (open: string[]) => void;
|
|
};
|
|
|
|
const Files = React.forwardRef<HTMLDivElement, FilesProps>(
|
|
({ children, className, defaultOpen, open, onOpenChange, ...props }, ref) => {
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={cn(
|
|
'relative size-full rounded-xl border bg-background overflow-auto',
|
|
className,
|
|
)}
|
|
{...props}
|
|
>
|
|
<MotionHighlight
|
|
controlledItems
|
|
mode="parent"
|
|
hover
|
|
className="bg-muted rounded-lg pointer-events-none"
|
|
>
|
|
<Accordion
|
|
type="multiple"
|
|
className="p-2"
|
|
defaultValue={defaultOpen}
|
|
value={open}
|
|
onValueChange={onOpenChange}
|
|
>
|
|
{children}
|
|
</Accordion>
|
|
</MotionHighlight>
|
|
</div>
|
|
);
|
|
},
|
|
);
|
|
Files.displayName = 'Files';
|
|
|
|
type FolderTriggerProps = AccordionTriggerProps & {
|
|
layoutId?: string;
|
|
};
|
|
|
|
const FolderTrigger = React.forwardRef<HTMLButtonElement, FolderTriggerProps>(
|
|
({ children, layoutId, ...props }, ref) => {
|
|
const { isOpen } = useAccordionItem();
|
|
|
|
return (
|
|
<AccordionTrigger
|
|
ref={ref}
|
|
className="h-auto py-0 hover:no-underline font-normal relative z-10"
|
|
{...props}
|
|
chevron={false}
|
|
>
|
|
<FileButton
|
|
open={isOpen}
|
|
icons={{ open: <FolderOpenIcon />, close: <FolderIcon /> }}
|
|
layoutId={layoutId}
|
|
>
|
|
{children}
|
|
</FileButton>
|
|
</AccordionTrigger>
|
|
);
|
|
},
|
|
);
|
|
FolderTrigger.displayName = 'FolderTrigger';
|
|
|
|
type FolderProps = Omit<AccordionItemProps, 'value'> & {
|
|
name: string;
|
|
open?: string[];
|
|
onOpenChange?: (open: string[]) => void;
|
|
defaultOpen?: string[];
|
|
layoutId?: string;
|
|
};
|
|
|
|
const Folder = React.forwardRef<HTMLDivElement, FolderProps>(
|
|
(
|
|
{ children, name, open, defaultOpen, onOpenChange, layoutId, ...props },
|
|
ref,
|
|
) => (
|
|
<AccordionItem
|
|
ref={ref}
|
|
value={name}
|
|
className="relative border-b-0"
|
|
{...props}
|
|
>
|
|
<FolderTrigger layoutId={layoutId}>{name}</FolderTrigger>
|
|
<AccordionContent className="relative pb-0 !ml-7 before:absolute before:-left-3 before:inset-y-0 before:w-px before:h-full before:bg-border">
|
|
<Accordion
|
|
type="multiple"
|
|
defaultValue={defaultOpen}
|
|
value={open}
|
|
onValueChange={onOpenChange}
|
|
>
|
|
{children}
|
|
</Accordion>
|
|
</AccordionContent>
|
|
</AccordionItem>
|
|
),
|
|
);
|
|
Folder.displayName = 'Folder';
|
|
|
|
type FileProps = Omit<HTMLMotionProps<'div'>, 'children'> & {
|
|
name: string;
|
|
layoutId?: string;
|
|
};
|
|
|
|
const File = React.forwardRef<HTMLDivElement, FileProps>(
|
|
({ name, layoutId, ...props }, ref) => (
|
|
<FileButton ref={ref} icon={<FileIcon />} layoutId={layoutId} {...props}>
|
|
{name}
|
|
</FileButton>
|
|
),
|
|
);
|
|
File.displayName = 'File';
|
|
|
|
export { Files, Folder, File };
|