"use client"; import { AnimatePresence, LayoutGroup, MotionConfig, motion } from "motion/react"; import { ArrowDownUpIcon, ChevronsUpDownIcon, ClockIcon, CloudUploadIcon, HouseIcon, LayoutGridIcon, ListIcon, PlusIcon, SearchIcon, StarIcon, Trash2Icon, UsersRoundIcon, XIcon, } from "lucide-react"; import { useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group"; import { Kbd } from "@/components/ui/kbd"; import { Menu, MenuGroup, MenuGroupLabel, MenuPopup, MenuRadioGroup, MenuRadioItem, MenuSeparator, MenuTrigger, } from "@/components/ui/menu"; import { cn } from "@/lib/utils"; export type FolderTone = "orange" | "yellow" | "lime" | "teal" | "blue" | "violet" | "rose" | "slate" | "neutral"; export interface FileFolder { id: string; name: string; files: number; sizeMb: number; tone: FolderTone; editedMinutesAgo: number; members: string[]; shared?: boolean; starred?: boolean; } export interface FileTemplate { id: string; name: string; kind: "doc" | "brief" | "sheet" | "roadmap" | "board" | "moodboard"; } export interface DriveProps { title?: string; folders?: FileFolder[]; templates?: FileTemplate[]; usedGb?: number; totalGb?: number; defaultView?: "grid" | "list"; workspace?: string; onOpenFolder?: (folder: FileFolder) => void; className?: string; } const tone: Record = { orange: "text-orange-500 dark:text-orange-400", yellow: "text-amber-500 dark:text-amber-400", lime: "text-lime-600", teal: "text-teal-600 dark:text-teal-500", blue: "text-blue-500 dark:text-blue-400", violet: "text-violet-500 dark:text-violet-400", rose: "text-rose-500 dark:text-rose-400", slate: "text-slate-500 dark:text-slate-400", neutral: "text-muted-foreground/64", }; const memberTone: Record = { AK: "bg-orange-500/16 text-orange-800 dark:text-orange-300", JS: "bg-teal-500/16 text-teal-700 dark:text-teal-300", MV: "bg-blue-500/16 text-blue-700 dark:text-blue-300", LO: "bg-violet-500/18 text-violet-700 dark:text-violet-300", RB: "bg-rose-500/16 text-rose-700 dark:text-rose-300", }; const spring = { type: "spring", stiffness: 380, damping: 34 } as const; const defaultFolders: FileFolder[] = [ { id: "brand", name: "Brand refresh", files: 128, sizeMb: 2380, tone: "orange", editedMinutesAgo: 12, members: ["AK", "JS", "MV"], shared: true, starred: true }, { id: "research", name: "User research", files: 64, sizeMb: 912, tone: "teal", editedMinutesAgo: 95, members: ["LO", "JS"], shared: true }, { id: "finance", name: "Finance 2026", files: 37, sizeMb: 48, tone: "lime", editedMinutesAgo: 1440, members: ["RB"] }, { id: "launch", name: "Orbit 3 launch", files: 211, sizeMb: 5120, tone: "rose", editedMinutesAgo: 30, members: ["AK", "MV", "LO", "RB"], shared: true, starred: true }, { id: "legal", name: "Contracts", files: 19, sizeMb: 22, tone: "slate", editedMinutesAgo: 8640, members: ["RB", "AK"] }, { id: "hiring", name: "Hiring loop", files: 42, sizeMb: 156, tone: "violet", editedMinutesAgo: 300, members: ["JS", "LO"], shared: true }, { id: "offsite", name: "Offsite photos", files: 486, sizeMb: 14200, tone: "yellow", editedMinutesAgo: 20160, members: ["MV"] }, { id: "eng", name: "Engineering RFCs", files: 73, sizeMb: 31, tone: "blue", editedMinutesAgo: 180, members: ["AK", "JS"], starred: true }, ]; const defaultTemplates: FileTemplate[] = [ { id: "notes", name: "Meeting notes", kind: "doc" }, { id: "brief", name: "Project brief", kind: "brief" }, { id: "budget", name: "Budget", kind: "sheet" }, { id: "roadmap", name: "Roadmap", kind: "roadmap" }, { id: "retro", name: "Retro board", kind: "board" }, { id: "mood", name: "Moodboard", kind: "moodboard" }, ]; function formatSize(mb: number) { return mb >= 1000 ? `${(mb / 1024).toFixed(1)} GB` : `${mb} MB`; } function formatAgo(min: number) { if (min < 1) return "Just now"; if (min < 60) return `${min}m ago`; if (min < 1440) return `${Math.round(min / 60)}h ago`; if (min < 10080) return `${Math.round(min / 1440)}d ago`; return `${Math.round(min / 10080)}w ago`; } function FolderGlyph({ folderTone, className, layoutId }: { folderTone: FolderTone; className?: string; layoutId?: string }) { return ( ); } function TemplatePreview({ kind }: { kind: FileTemplate["kind"] }) { const line = "h-[3px] rounded-full bg-foreground/12"; switch (kind) { case "doc": return ( {[92, 80, 86].map((w) => ( ))} ); case "brief": return ( ); case "sheet": return ( {Array.from({ length: 12 }, (_, i) => ( ))} ); case "roadmap": return ( {[ [0, 50, "bg-blue-500/40"], [24, 48, "bg-teal-500/40"], [50, 44, "bg-violet-500/40"], ].map(([l, w, c]) => ( ))} ); case "board": return ( {["bg-amber-400/40", "bg-teal-500/30", "bg-violet-500/30", "bg-violet-500/30", "bg-amber-400/40", "bg-foreground/10"].map((c, i) => ( ))} ); case "moodboard": return ( ); } } function Members({ ids }: { ids: string[] }) { if (ids.length === 0) return Only you; return ( {ids.slice(0, 3).map((m) => ( {m} ))} {ids.length > 3 && ( +{ids.length - 3} )} ); } type SortKey = "name" | "edited" | "size" | "files"; type Tab = "all" | "recent" | "shared" | "starred"; function useThemeRadius(scale: number) { const probe = useRef(null); const [radius, setRadius] = useState(scale * 10); useLayoutEffect(() => { const el = probe.current; if (!el) return; const read = () => setRadius(el.offsetWidth); read(); const observer = new ResizeObserver(read); observer.observe(el); return () => observer.disconnect(); }, []); const marker = ( ); return [radius, marker] as const; } export function Drive({ title = "Files", folders: initialFolders = defaultFolders, templates = defaultTemplates, usedGb = 42.6, totalGb = 100, defaultView = "grid", workspace = "Northwind", onOpenFolder, className, }: DriveProps) { const [radius, radiusProbe] = useThemeRadius(1.2); const [folders, setFolders] = useState(initialFolders); const [tab, setTab] = useState("all"); const [query, setQuery] = useState(""); const [sort, setSort] = useState("edited"); const [dir, setDir] = useState<"asc" | "desc">("desc"); const [view, setView] = useState(defaultView); const [created, setCreated] = useState(null); const group = useId(); useEffect(() => { if (!created) return; const t = setTimeout(() => setCreated(null), 2800); return () => clearTimeout(t); }, [created]); const tabs = [ { id: "all", label: "All files", icon: HouseIcon, count: folders.length }, { id: "recent", label: "Recent", icon: ClockIcon, count: folders.filter((f) => f.editedMinutesAgo < 1440).length }, { id: "shared", label: "Shared", icon: UsersRoundIcon, count: folders.filter((f) => f.shared).length }, { id: "starred", label: "Starred", icon: StarIcon, count: folders.filter((f) => f.starred).length }, ] as const; const visible = useMemo(() => { const q = query.trim().toLowerCase(); const list = folders.filter((f) => { if (tab === "recent" && f.editedMinutesAgo >= 1440) return false; if (tab === "shared" && !f.shared) return false; if (tab === "starred" && !f.starred) return false; return !q || f.name.toLowerCase().includes(q); }); const by: Record number> = { name: (a, b) => a.name.localeCompare(b.name), edited: (a, b) => b.editedMinutesAgo - a.editedMinutesAgo, size: (a, b) => a.sizeMb - b.sizeMb, files: (a, b) => a.files - b.files, }; const sorted = [...list].sort(by[sort]); return dir === "desc" ? sorted.reverse() : sorted; }, [folders, tab, query, sort, dir]); const newFolder = () => { const n = folders.filter((f) => f.id.startsWith("new-")).length + 1; const name = n === 1 ? "Untitled folder" : `Untitled folder ${n}`; setFolders((fs) => [{ id: `new-${n}`, name, files: 0, sizeMb: 0, tone: "neutral", editedMinutesAgo: 0, members: [] }, ...fs]); setTab("all"); setQuery(""); setCreated(name); }; const toggleStar = (id: string) => setFolders((fs) => fs.map((f) => (f.id === id ? { ...f, starred: !f.starred } : f))); const sortLabel = { name: "Name", edited: "Last edited", size: "Size", files: "File count" }[sort]; const usedPct = Math.min(100, (usedGb / totalGb) * 100); const grid = view === "grid"; const heading = tab === "all" ? title : tabs.find((t) => t.id === tab)?.label; const starred = folders.filter((f) => f.starred); return (
setQuery(e.target.value)} placeholder="Search folders" value={query} /> {query ? ( ) : ( / )}

{heading}

{tabs.map((t) => ( ))}
{tab === "all" && !query && templates.length > 0 && (

Start something new

{templates.map((t) => ( ))}
)}

Folders

{visible.length}
}> {sortLabel} Sort by setSort(v as SortKey)} value={sort}> Name Last edited Size File count Order setDir(v as "asc" | "desc")} value={dir}> {sort === "name" ? "A to Z" : sort === "edited" ? "Oldest first" : "Smallest first"} {sort === "name" ? "Z to A" : sort === "edited" ? "Newest first" : "Largest first"}
{( [ ["grid", LayoutGridIcon, "Grid"], ["list", ListIcon, "List"], ] as const ).map(([v, Icon, label]) => ( ))}
{radiusProbe} {!grid && visible.length > 0 && (
Name Shared with Edited Size
)}
    {visible.map((f, i) => ( [data-hit]:active]:scale-[0.985]", grid ? "rounded-xl border bg-card hover:border-foreground/16" : cn("hover:bg-accent/40", i > 0 && "border-t"), )} exit={{ opacity: 0, scale: 0.96 }} initial={{ opacity: 0, scale: 0.96 }} key={f.id} layout style={{ borderRadius: grid ? radius : 0 }} transition={spring} >
{visible.length === 0 && (
No folders match {query ? `Nothing called “${query.trim()}”` : "Nothing here yet"} {tab !== "all" && ` in ${tabs.find((t) => t.id === tab)?.label}`}.
)}
{created && ( {created} {created !== "Upload started" && " created"} )}
); }