2025-08-20 04:12:49 -06:00

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 };