import type { ComponentPropsWithRef } from "react"; import { AnimatePresence, m as motion } from "motion/react"; import { cn } from "../../lib/utils"; type DivProps = ComponentPropsWithRef<"div">; type Attachment = { /** Unique identifier. */ id: string; /** Display name. */ name: string; /** MIME type. */ type: string; /** File size in bytes. */ size?: number; /** Preview URL for images/videos. */ preview?: string; }; const ROTATIONS = [-2, 1.5, -1, 2, -1.5]; const EDGE = "pointer-events-none absolute inset-0 rounded-[inherit] squircle shadow-[inset_0_0_0_1px_rgb(0_0_0/0.18)] dark:shadow-[inset_0_0_0_1px_rgb(255_255_255/0.1)]"; const item = { hidden: { opacity: 0, scale: 0.9, y: 15, rotate: 0 }, visible: (i: number) => ({ opacity: 1, scale: 1, y: 0, rotate: ROTATIONS[i % ROTATIONS.length], transition: { type: "spring" as const, stiffness: 500, damping: 25, delay: 0.1 + i * 0.05, rotate: { type: "spring" as const, stiffness: 500, damping: 25 }, }, }), exit: { opacity: 0, scale: 0.95, transition: { duration: 0.15 } }, }; /** Individual attachment preview card. */ function Item( { attachment, index, onRemove }: { attachment: Attachment; index: number; onRemove?: (id: string) => void; }, ) { let image = attachment.type.startsWith("image/"); let video = attachment.type.startsWith("video/"); let media = (image || video) && attachment.preview; return ( {image && attachment.preview && (
{attachment.name}
)} {video && attachment.preview && (
)} {!image && !video && (
{extension(attachment.name)}
)} {onRemove && ( )}
); } function extension(name: string) { let dot = name.lastIndexOf("."); return dot >= 0 ? name.slice(dot) : name; } /** Horizontally scrollable list of attachment previews with scroll fade. */ function AttachmentList({ attachments, onRemove, className, ref, ...props }: DivProps & { attachments: Attachment[]; onRemove?: (id: string) => void; }) { if (!attachments.length) return null; return (
{attachments.map((a, i) => ( ))}
); } export { type Attachment, AttachmentList };