File Manager Recent
Original · freerecent files list
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/filemgr-recent.json"use client";
import type { KeyboardEvent as ReactKeyboardEvent } from "react";
import { useCallback, useMemo, useRef, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
type FileKind = "doc" | "sheet" | "image" | "video" | "code" | "archive" | "pdf";
type RecentFile = {
id: string;
name: string;
kind: FileKind;
folder: string;
size: string;
editor: string;
openedAt: number;
action: "edited" | "viewed" | "shared" | "uploaded";
};
const NOW = Date.UTC(2026, 6, 17, 14, 30, 0);
const MIN = 60_000;
const HOUR = 60 * MIN;
const DAY = 24 * HOUR;
const FILES: RecentFile[] = [
{
id: "f-01",
name: "Q3-roadmap-review.pdf",
kind: "pdf",
folder: "Planning / 2026",
size: "2.4 MB",
editor: "Nadia Rahman",
openedAt: NOW - 8 * MIN,
action: "viewed",
},
{
id: "f-02",
name: "pricing-experiment.tsx",
kind: "code",
folder: "web/src/components",
size: "18 KB",
editor: "You",
openedAt: NOW - 41 * MIN,
action: "edited",
},
{
id: "f-03",
name: "churn-cohorts-june.xlsx",
kind: "sheet",
folder: "Analytics / Retention",
size: "864 KB",
editor: "Marcus Vidal",
openedAt: NOW - 3 * HOUR,
action: "shared",
},
{
id: "f-04",
name: "onboarding-walkthrough.mp4",
kind: "video",
folder: "Marketing / Demos",
size: "148 MB",
editor: "Priya Anand",
openedAt: NOW - 6 * HOUR,
action: "uploaded",
},
{
id: "f-05",
name: "brand-lockup-v9.png",
kind: "image",
folder: "Design / Identity",
size: "3.1 MB",
editor: "You",
openedAt: NOW - 27 * HOUR,
action: "edited",
},
{
id: "f-06",
name: "vendor-contract-signed.pdf",
kind: "pdf",
folder: "Legal / Vendors",
size: "512 KB",
editor: "Ilse Brandt",
openedAt: NOW - 2 * DAY - 4 * HOUR,
action: "shared",
},
{
id: "f-07",
name: "support-macros.docx",
kind: "doc",
folder: "Ops / Playbooks",
size: "96 KB",
editor: "Tomas Reyes",
openedAt: NOW - 4 * DAY,
action: "edited",
},
{
id: "f-08",
name: "legacy-exports-2025.zip",
kind: "archive",
folder: "Archive",
size: "1.2 GB",
editor: "You",
openedAt: NOW - 9 * DAY,
action: "uploaded",
},
{
id: "f-09",
name: "api-error-budget.xlsx",
kind: "sheet",
folder: "Engineering / SLOs",
size: "240 KB",
editor: "Marcus Vidal",
openedAt: NOW - 12 * DAY,
action: "viewed",
},
{
id: "f-10",
name: "field-notes-sydney.docx",
kind: "doc",
folder: "Research / Interviews",
size: "74 KB",
editor: "Priya Anand",
openedAt: NOW - 21 * DAY,
action: "edited",
},
];
type FilterId = "all" | "mine" | "shared" | "media";
const FILTERS: { id: FilterId; label: string }[] = [
{ id: "all", label: "All activity" },
{ id: "mine", label: "Edited by me" },
{ id: "shared", label: "Shared with me" },
{ id: "media", label: "Media" },
];
const KIND_META: Record<
FileKind,
{ label: string; tint: string; ring: string }
> = {
doc: {
label: "Document",
tint: "bg-sky-100 text-sky-700 dark:bg-sky-500/15 dark:text-sky-300",
ring: "ring-sky-500/20 dark:ring-sky-400/25",
},
sheet: {
label: "Spreadsheet",
tint: "bg-emerald-100 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300",
ring: "ring-emerald-500/20 dark:ring-emerald-400/25",
},
image: {
label: "Image",
tint: "bg-violet-100 text-violet-700 dark:bg-violet-500/15 dark:text-violet-300",
ring: "ring-violet-500/20 dark:ring-violet-400/25",
},
video: {
label: "Video",
tint: "bg-rose-100 text-rose-700 dark:bg-rose-500/15 dark:text-rose-300",
ring: "ring-rose-500/20 dark:ring-rose-400/25",
},
code: {
label: "Source file",
tint: "bg-indigo-100 text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-300",
ring: "ring-indigo-500/20 dark:ring-indigo-400/25",
},
archive: {
label: "Archive",
tint: "bg-amber-100 text-amber-800 dark:bg-amber-500/15 dark:text-amber-300",
ring: "ring-amber-500/20 dark:ring-amber-400/25",
},
pdf: {
label: "PDF",
tint: "bg-rose-100 text-rose-700 dark:bg-rose-500/15 dark:text-rose-300",
ring: "ring-rose-500/20 dark:ring-rose-400/25",
},
};
function KindIcon({ kind }: { kind: FileKind }) {
const common = {
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: 1.6,
strokeLinecap: "round" as const,
strokeLinejoin: "round" as const,
"aria-hidden": true,
className: "h-[18px] w-[18px]",
};
switch (kind) {
case "sheet":
return (
<svg {...common}>
<rect x="3" y="4" width="18" height="16" rx="2" />
<path d="M3 10h18M9 10v10M3 15h18" />
</svg>
);
case "image":
return (
<svg {...common}>
<rect x="3" y="4" width="18" height="16" rx="2" />
<circle cx="8.5" cy="9.5" r="1.5" />
<path d="m4 17 4.5-4.5a1.5 1.5 0 0 1 2.1 0L20 20" />
</svg>
);
case "video":
return (
<svg {...common}>
<rect x="2.5" y="5" width="14" height="14" rx="2.5" />
<path d="m16.5 10.5 5-3v9l-5-3z" />
</svg>
);
case "code":
return (
<svg {...common}>
<path d="m9 8-4.5 4L9 16M15 8l4.5 4L15 16M13.5 5.5l-3 13" />
</svg>
);
case "archive":
return (
<svg {...common}>
<path d="M3 7.5 5 4h14l2 3.5V19a1.5 1.5 0 0 1-1.5 1.5h-15A1.5 1.5 0 0 1 3 19z" />
<path d="M3 7.5h18M10 11.5h4M10 15h4" />
</svg>
);
case "pdf":
return (
<svg {...common}>
<path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z" />
<path d="M14 3v5h5M8.5 16.5c2.5-1 4-4.5 3.2-6-.7-1.3-1.9-.4-1.5 1.4.5 2.4 3 4.9 5.3 4.6" />
</svg>
);
default:
return (
<svg {...common}>
<path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z" />
<path d="M14 3v5h5M9 13h6M9 17h4" />
</svg>
);
}
}
function StarIcon({ filled }: { filled: boolean }) {
return (
<svg
viewBox="0 0 24 24"
fill={filled ? "currentColor" : "none"}
stroke="currentColor"
strokeWidth={1.6}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
className="h-[17px] w-[17px]"
>
<path d="m12 3.6 2.6 5.3 5.8.85-4.2 4.1 1 5.75L12 16.9l-5.2 2.7 1-5.75-4.2-4.1 5.8-.85z" />
</svg>
);
}
function relativeTime(ts: number): string {
const diff = NOW - ts;
if (diff < HOUR) return `${Math.max(1, Math.round(diff / MIN))} min ago`;
if (diff < DAY) return `${Math.round(diff / HOUR)} hr ago`;
const days = Math.round(diff / DAY);
if (days === 1) return "Yesterday";
if (days < 7) return `${days} days ago`;
if (days < 30) return `${Math.round(days / 7)} wk ago`;
return `${Math.round(days / 30)} mo ago`;
}
function bucketOf(ts: number): string {
const diff = NOW - ts;
if (diff < DAY) return "Today";
if (diff < 7 * DAY) return "Earlier this week";
if (diff < 30 * DAY) return "Earlier this month";
return "Older";
}
const BUCKET_ORDER = ["Today", "Earlier this week", "Earlier this month", "Older"];
function absoluteTime(ts: number): string {
return new Date(ts).toLocaleString("en-GB", {
day: "numeric",
month: "short",
hour: "2-digit",
minute: "2-digit",
timeZone: "UTC",
});
}
export default function FilemgrRecent() {
const reduce = useReducedMotion();
const [filter, setFilter] = useState<FilterId>("all");
const [query, setQuery] = useState("");
const [starred, setStarred] = useState<Record<string, boolean>>({
"f-02": true,
"f-06": true,
});
const [activeId, setActiveId] = useState<string>(FILES[0].id);
const [openId, setOpenId] = useState<string | null>(null);
const [toast, setToast] = useState<string | null>(null);
const rowRefs = useRef<Record<string, HTMLButtonElement | null>>({});
const toastTimer = useRef<number | null>(null);
const announce = useCallback((message: string) => {
setToast(message);
if (toastTimer.current !== null) window.clearTimeout(toastTimer.current);
toastTimer.current = window.setTimeout(() => setToast(null), 2600);
}, []);
const visible = useMemo(() => {
const q = query.trim().toLowerCase();
return FILES.filter((f) => {
if (filter === "mine" && f.editor !== "You") return false;
if (filter === "shared" && (f.action !== "shared" || f.editor === "You"))
return false;
if (filter === "media" && !["image", "video"].includes(f.kind)) return false;
if (!q) return true;
return (
f.name.toLowerCase().includes(q) || f.folder.toLowerCase().includes(q)
);
}).sort((a, b) => b.openedAt - a.openedAt);
}, [filter, query]);
const groups = useMemo(() => {
const map = new Map<string, RecentFile[]>();
for (const f of visible) {
const b = bucketOf(f.openedAt);
const list = map.get(b);
if (list) list.push(f);
else map.set(b, [f]);
}
return BUCKET_ORDER.filter((b) => map.has(b)).map((b) => ({
bucket: b,
items: map.get(b) as RecentFile[],
}));
}, [visible]);
const flat = visible;
const focusRow = useCallback((id: string) => {
setActiveId(id);
rowRefs.current[id]?.focus();
}, []);
const onListKeyDown = useCallback(
(event: ReactKeyboardEvent<HTMLDivElement>) => {
if (flat.length === 0) return;
const index = flat.findIndex((f) => f.id === activeId);
const current = index === -1 ? 0 : index;
if (event.key === "ArrowDown") {
event.preventDefault();
focusRow(flat[Math.min(flat.length - 1, current + 1)].id);
} else if (event.key === "ArrowUp") {
event.preventDefault();
focusRow(flat[Math.max(0, current - 1)].id);
} else if (event.key === "Home") {
event.preventDefault();
focusRow(flat[0].id);
} else if (event.key === "End") {
event.preventDefault();
focusRow(flat[flat.length - 1].id);
}
},
[activeId, flat, focusRow],
);
const toggleStar = useCallback(
(file: RecentFile) => {
setStarred((prev) => ({ ...prev, [file.id]: !prev[file.id] }));
announce(
starred[file.id]
? `Removed ${file.name} from starred`
: `Starred ${file.name}`,
);
},
[announce, starred],
);
return (
<section className="relative w-full bg-slate-50 px-4 py-16 sm:px-6 sm:py-20 dark:bg-slate-950">
<style>{`
@keyframes fmr-pulse-dot {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.9); opacity: 0; }
}
@keyframes fmr-sweep {
0% { transform: translateX(-100%); }
100% { transform: translateX(220%); }
}
.fmr-dot::after {
content: "";
position: absolute;
inset: 0;
border-radius: 9999px;
background: currentColor;
animation: fmr-pulse-dot 2.4s ease-out infinite;
}
.fmr-sweep::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
width: 40%;
background: linear-gradient(90deg, transparent, rgba(99,102,241,0.14), transparent);
animation: fmr-sweep 2.8s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.fmr-dot::after, .fmr-sweep::before { animation: none; }
.fmr-sweep::before { display: none; }
}
`}</style>
<div className="mx-auto w-full max-w-4xl">
<header className="mb-7 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
<div>
<p className="mb-2 inline-flex items-center gap-2 rounded-full bg-indigo-50 px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.14em] text-indigo-700 ring-1 ring-inset ring-indigo-500/20 dark:bg-indigo-500/10 dark:text-indigo-300 dark:ring-indigo-400/25">
<span className="fmr-dot relative inline-block h-1.5 w-1.5 rounded-full bg-indigo-500 text-indigo-500 dark:bg-indigo-400 dark:text-indigo-400" />
Live workspace
</p>
<h2 className="text-2xl font-semibold tracking-tight text-slate-900 sm:text-[28px] dark:text-slate-50">
Recent files
</h2>
<p className="mt-1.5 text-sm text-slate-600 dark:text-slate-400">
Everything you and the team touched across Drive, sorted by last
activity.
</p>
</div>
<div className="relative w-full sm:w-64">
<label htmlFor="fmr-search" className="sr-only">
Filter recent files by name or folder
</label>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.7}
strokeLinecap="round"
aria-hidden
className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400 dark:text-slate-500"
>
<circle cx="11" cy="11" r="6.5" />
<path d="m16 16 4.5 4.5" />
</svg>
<input
id="fmr-search"
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search name or folder"
className="w-full rounded-xl border border-slate-200 bg-white py-2.5 pl-9 pr-3 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-100 dark:placeholder:text-slate-500 dark:focus-visible:ring-offset-slate-950"
/>
</div>
</header>
<div
role="tablist"
aria-label="Filter recent files"
className="mb-5 flex flex-wrap gap-1.5"
>
{FILTERS.map((f) => {
const selected = filter === f.id;
return (
<button
key={f.id}
type="button"
role="tab"
id={`fmr-tab-${f.id}`}
aria-selected={selected}
aria-controls="fmr-list"
onClick={() => setFilter(f.id)}
className={`relative rounded-lg px-3 py-1.5 text-[13px] font-medium transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:focus-visible:ring-offset-slate-950 ${
selected
? "text-white dark:text-slate-950"
: "text-slate-600 hover:bg-slate-200/60 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-slate-800/70 dark:hover:text-slate-100"
}`}
>
{selected && (
<motion.span
layoutId="fmr-tab-pill"
transition={
reduce
? { duration: 0 }
: { type: "spring", stiffness: 420, damping: 34 }
}
className="absolute inset-0 -z-10 rounded-lg bg-slate-900 dark:bg-slate-100"
/>
)}
{f.label}
</button>
);
})}
</div>
<div
id="fmr-list"
role="region"
aria-live="polite"
onKeyDown={onListKeyDown}
className="fmr-sweep relative overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900"
>
{groups.length === 0 && (
<div className="px-6 py-16 text-center">
<p className="text-sm font-medium text-slate-900 dark:text-slate-100">
No files match “{query || FILTERS.find((f) => f.id === filter)?.label}”
</p>
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
Try a different filter or clear the search box.
</p>
<button
type="button"
onClick={() => {
setQuery("");
setFilter("all");
}}
className="mt-4 rounded-lg border border-slate-200 px-3 py-1.5 text-[13px] font-medium text-slate-700 transition-colors hover:bg-slate-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-800 dark:focus-visible:ring-offset-slate-900"
>
Reset view
</button>
</div>
)}
{groups.map((group) => (
<div key={group.bucket}>
<h3 className="sticky top-0 z-10 border-b border-slate-200/80 bg-slate-50/90 px-4 py-2 text-[11px] font-semibold uppercase tracking-[0.12em] text-slate-500 backdrop-blur sm:px-5 dark:border-slate-800 dark:bg-slate-900/90 dark:text-slate-400">
{group.bucket}
<span className="ml-2 font-normal normal-case tracking-normal text-slate-400 dark:text-slate-500">
{group.items.length}
</span>
</h3>
<ul className="divide-y divide-slate-100 dark:divide-slate-800">
{group.items.map((file) => {
const meta = KIND_META[file.kind];
const isOpen = openId === file.id;
const isStar = Boolean(starred[file.id]);
return (
<li key={file.id} className="relative">
<div className="flex items-center gap-3 px-3 py-2.5 transition-colors hover:bg-slate-50 sm:px-4 dark:hover:bg-slate-800/50">
<button
type="button"
ref={(el) => {
rowRefs.current[file.id] = el;
}}
tabIndex={activeId === file.id ? 0 : -1}
aria-expanded={isOpen}
aria-controls={`fmr-panel-${file.id}`}
onFocus={() => setActiveId(file.id)}
onClick={() => {
setActiveId(file.id);
setOpenId(isOpen ? null : file.id);
}}
className="flex min-w-0 flex-1 items-center gap-3 rounded-lg py-1 text-left focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-slate-900"
>
<span
className={`grid h-9 w-9 shrink-0 place-items-center rounded-lg ring-1 ring-inset ${meta.tint} ${meta.ring}`}
>
<KindIcon kind={file.kind} />
</span>
<span className="min-w-0 flex-1">
<span className="flex items-center gap-1.5">
<span className="truncate text-[14px] font-medium text-slate-900 dark:text-slate-100">
{file.name}
</span>
{isStar && (
<span className="shrink-0 text-amber-500 dark:text-amber-400">
<svg
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden
className="h-3 w-3"
>
<path d="m12 3.6 2.6 5.3 5.8.85-4.2 4.1 1 5.75L12 16.9l-5.2 2.7 1-5.75-4.2-4.1 5.8-.85z" />
</svg>
</span>
)}
</span>
<span className="mt-0.5 flex items-center gap-1.5 text-[12px] text-slate-500 dark:text-slate-400">
<span className="truncate">{file.folder}</span>
<span aria-hidden className="text-slate-300 dark:text-slate-600">
·
</span>
<span className="shrink-0 tabular-nums">{file.size}</span>
</span>
</span>
<span className="hidden shrink-0 text-right sm:block">
<span className="block text-[12px] text-slate-600 dark:text-slate-300">
{file.editor === "You" ? "You" : file.editor}
</span>
<time
dateTime={new Date(file.openedAt).toISOString()}
title={absoluteTime(file.openedAt)}
className="block text-[11px] text-slate-400 dark:text-slate-500"
>
{relativeTime(file.openedAt)}
</time>
</span>
</button>
<button
type="button"
aria-pressed={isStar}
onClick={() => toggleStar(file)}
className={`shrink-0 rounded-lg p-2 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-slate-900 ${
isStar
? "text-amber-500 hover:bg-amber-50 dark:text-amber-400 dark:hover:bg-amber-500/10"
: "text-slate-400 hover:bg-slate-100 hover:text-slate-600 dark:text-slate-500 dark:hover:bg-slate-800 dark:hover:text-slate-300"
}`}
>
<StarIcon filled={isStar} />
<span className="sr-only">
{isStar ? `Unstar ${file.name}` : `Star ${file.name}`}
</span>
</button>
</div>
<AnimatePresence initial={false}>
{isOpen && (
<motion.div
key="panel"
id={`fmr-panel-${file.id}`}
role="region"
aria-label={`Details for ${file.name}`}
initial={reduce ? false : { height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={reduce ? { opacity: 0 } : { height: 0, opacity: 0 }}
transition={
reduce
? { duration: 0 }
: { duration: 0.24, ease: [0.22, 1, 0.36, 1] }
}
className="overflow-hidden"
>
<div className="mx-3 mb-3 rounded-xl bg-slate-50 p-3.5 ring-1 ring-inset ring-slate-200 sm:mx-4 dark:bg-slate-950/40 dark:ring-slate-800">
<dl className="grid grid-cols-2 gap-x-4 gap-y-2.5 text-[12px] sm:grid-cols-4">
<div>
<dt className="text-slate-500 dark:text-slate-400">Type</dt>
<dd className="mt-0.5 font-medium text-slate-800 dark:text-slate-200">
{meta.label}
</dd>
</div>
<div>
<dt className="text-slate-500 dark:text-slate-400">
Last action
</dt>
<dd className="mt-0.5 font-medium capitalize text-slate-800 dark:text-slate-200">
{file.action}
</dd>
</div>
<div>
<dt className="text-slate-500 dark:text-slate-400">
Opened
</dt>
<dd className="mt-0.5 font-medium tabular-nums text-slate-800 dark:text-slate-200">
{absoluteTime(file.openedAt)} UTC
</dd>
</div>
<div>
<dt className="text-slate-500 dark:text-slate-400">
Location
</dt>
<dd className="mt-0.5 truncate font-medium text-slate-800 dark:text-slate-200">
{file.folder}
</dd>
</div>
</dl>
<div className="mt-3.5 flex flex-wrap gap-2">
<button
type="button"
onClick={() => announce(`Opening ${file.name}…`)}
className="rounded-lg bg-slate-900 px-3 py-1.5 text-[12px] font-medium text-white transition-colors hover:bg-slate-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 dark:bg-slate-100 dark:text-slate-900 dark:hover:bg-white dark:focus-visible:ring-offset-slate-900"
>
Open file
</button>
<button
type="button"
onClick={() =>
announce(`Copied link to ${file.name}`)
}
className="rounded-lg border border-slate-200 px-3 py-1.5 text-[12px] font-medium text-slate-700 transition-colors hover:bg-white focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-800 dark:focus-visible:ring-offset-slate-900"
>
Copy link
</button>
<button
type="button"
onClick={() =>
announce(`${file.name} removed from Recent`)
}
className="rounded-lg border border-slate-200 px-3 py-1.5 text-[12px] font-medium text-slate-700 transition-colors hover:bg-white focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-800 dark:focus-visible:ring-offset-slate-900"
>
Remove from Recent
</button>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</li>
);
})}
</ul>
</div>
))}
</div>
<div className="mt-4 flex items-center justify-between gap-4">
<p className="text-[12px] text-slate-500 dark:text-slate-400">
Showing {visible.length} of {FILES.length} files · use ↑ ↓ to move, Enter
to expand
</p>
<p
role="status"
aria-live="polite"
className="min-h-[1rem] text-[12px] font-medium text-indigo-700 dark:text-indigo-300"
>
{toast}
</p>
</div>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
File Manager List
Originalfile manager list view

File Manager Grid
Originalfile manager grid view

File Manager Upload
Originalupload dropzone with progress

File Manager Breadcrumb
Originalfile path breadcrumb with toolbar

File Manager Tree
Originalfolder tree sidebar

File Manager Preview
Originalfile preview panel

File Manager Storage
Originalstorage usage widget

Spotlight Hero
OriginalA centred hero with a soft radial spotlight, badge and dual call-to-action.

Split Hero
OriginalA two-column hero pairing a headline and CTAs with a product mock and social proof.

Gradient Spotlight Hero
OriginalA minimal centred hero with a soft gradient-mesh backdrop, announcement pill and dual call-to-action buttons.

App Preview Hero
OriginalA centred hero that pairs headline copy with a realistic product dashboard mock built entirely from markup, complete with browser chrome and a floating notification card.

Waitlist Capture Hero
OriginalA dark, focused hero with an inline email capture form and avatar social proof, ready for pre-launch waitlists.

