File Card
Original · freeA file card with type icon, size and a download action.
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/card-file.json"use client";
import { useEffect, useRef, useState, type ReactNode } from "react";
import { motion, AnimatePresence, useReducedMotion } from "motion/react";
type FileKind = "pdf" | "zip" | "image" | "doc" | "sheet" | "video" | "audio" | "code";
type FileItem = {
id: string;
name: string;
kind: FileKind;
bytes: number;
meta: string;
};
const FILES: FileItem[] = [
{ id: "f1", name: "Brand-Guidelines-2026.pdf", kind: "pdf", bytes: 4_612_000, meta: "PDF · 24 pages" },
{ id: "f2", name: "Q3-Launch-Assets.zip", kind: "zip", bytes: 268_400_000, meta: "ZIP · 148 files" },
{ id: "f3", name: "hero-render-4k.png", kind: "image", bytes: 12_940_000, meta: "PNG · 3840×2160" },
{ id: "f4", name: "Investor-Memo-Final.docx", kind: "doc", bytes: 842_000, meta: "DOCX · 11 pages" },
{ id: "f5", name: "Revenue-Model-FY26.xlsx", kind: "sheet", bytes: 1_540_000, meta: "XLSX · 9 sheets" },
{ id: "f6", name: "Product-Walkthrough.mp4", kind: "video", bytes: 96_300_000, meta: "MP4 · 3:42" },
{ id: "f7", name: "Podcast-Ep-47-Master.wav", kind: "audio", bytes: 58_100_000, meta: "WAV · 41:08" },
{ id: "f8", name: "auth-service.tar.gz", kind: "code", bytes: 3_205_000, meta: "GZIP · source" },
];
function formatBytes(bytes: number): string {
const units = ["B", "KB", "MB", "GB"];
let value = bytes;
let unit = 0;
while (value >= 1024 && unit < units.length - 1) {
value /= 1024;
unit += 1;
}
const rounded = value >= 100 || unit === 0 ? Math.round(value) : Math.round(value * 10) / 10;
return `${rounded} ${units[unit]}`;
}
type KindStyle = {
label: string;
tint: string;
ring: string;
glyph: ReactNode;
};
function kindStyle(kind: FileKind): KindStyle {
const common = "h-6 w-6";
switch (kind) {
case "pdf":
return {
label: "PDF",
tint: "bg-rose-100 text-rose-600 dark:bg-rose-500/15 dark:text-rose-300",
ring: "group-hover:ring-rose-300/70 dark:group-hover:ring-rose-500/40",
glyph: (
<svg viewBox="0 0 24 24" fill="none" className={common} aria-hidden="true">
<path d="M6 2.75h7.5L18.25 7.5V19a2.25 2.25 0 0 1-2.25 2.25H6A2.25 2.25 0 0 1 3.75 19V5A2.25 2.25 0 0 1 6 2.75Z" stroke="currentColor" strokeWidth="1.5" />
<path d="M13 2.75V7a1 1 0 0 0 1 1h4.25" stroke="currentColor" strokeWidth="1.5" />
<path d="M7.5 16.5v-3.25h1.1a1 1 0 0 1 0 2H7.5m5-2h1.75m-1.75 0v3.25m0-1.6h1.4m2.35-1.65V16.5" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
</svg>
),
};
case "zip":
return {
label: "ZIP",
tint: "bg-amber-100 text-amber-600 dark:bg-amber-500/15 dark:text-amber-300",
ring: "group-hover:ring-amber-300/70 dark:group-hover:ring-amber-500/40",
glyph: (
<svg viewBox="0 0 24 24" fill="none" className={common} aria-hidden="true">
<path d="M6 2.75h12A2.25 2.25 0 0 1 20.25 5v14A2.25 2.25 0 0 1 18 21.25H6A2.25 2.25 0 0 1 3.75 19V5A2.25 2.25 0 0 1 6 2.75Z" stroke="currentColor" strokeWidth="1.5" />
<path d="M11.5 3v2m1 1v2m-1 1v2m1 1v2.5a1 1 0 0 1-2 0V13a1 1 0 0 1 2 0Z" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
</svg>
),
};
case "image":
return {
label: "IMG",
tint: "bg-violet-100 text-violet-600 dark:bg-violet-500/15 dark:text-violet-300",
ring: "group-hover:ring-violet-300/70 dark:group-hover:ring-violet-500/40",
glyph: (
<svg viewBox="0 0 24 24" fill="none" className={common} aria-hidden="true">
<rect x="3.75" y="4.75" width="16.5" height="14.5" rx="2.25" stroke="currentColor" strokeWidth="1.5" />
<circle cx="9" cy="9.5" r="1.5" stroke="currentColor" strokeWidth="1.4" />
<path d="m4.5 17 4.25-4a1.5 1.5 0 0 1 2 0l3 2.75m2-2.25 3.75 3.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
),
};
case "doc":
return {
label: "DOC",
tint: "bg-sky-100 text-sky-600 dark:bg-sky-500/15 dark:text-sky-300",
ring: "group-hover:ring-sky-300/70 dark:group-hover:ring-sky-500/40",
glyph: (
<svg viewBox="0 0 24 24" fill="none" className={common} aria-hidden="true">
<path d="M6 2.75h7.5L18.25 7.5V19a2.25 2.25 0 0 1-2.25 2.25H6A2.25 2.25 0 0 1 3.75 19V5A2.25 2.25 0 0 1 6 2.75Z" stroke="currentColor" strokeWidth="1.5" />
<path d="M13 2.75V7a1 1 0 0 0 1 1h4.25" stroke="currentColor" strokeWidth="1.5" />
<path d="M7.5 12.5h7M7.5 15.5h7M7.5 18h4" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
</svg>
),
};
case "sheet":
return {
label: "XLS",
tint: "bg-emerald-100 text-emerald-600 dark:bg-emerald-500/15 dark:text-emerald-300",
ring: "group-hover:ring-emerald-300/70 dark:group-hover:ring-emerald-500/40",
glyph: (
<svg viewBox="0 0 24 24" fill="none" className={common} aria-hidden="true">
<rect x="3.75" y="3.75" width="16.5" height="16.5" rx="2.25" stroke="currentColor" strokeWidth="1.5" />
<path d="M3.75 9h16.5M3.75 14.25h16.5M9.25 9v11.25M14.75 9v11.25" stroke="currentColor" strokeWidth="1.3" />
</svg>
),
};
case "video":
return {
label: "VID",
tint: "bg-indigo-100 text-indigo-600 dark:bg-indigo-500/15 dark:text-indigo-300",
ring: "group-hover:ring-indigo-300/70 dark:group-hover:ring-indigo-500/40",
glyph: (
<svg viewBox="0 0 24 24" fill="none" className={common} aria-hidden="true">
<rect x="3.75" y="5.75" width="16.5" height="12.5" rx="2.25" stroke="currentColor" strokeWidth="1.5" />
<path d="M10.25 9.75 14 12l-3.75 2.25v-4.5Z" fill="currentColor" />
</svg>
),
};
case "audio":
return {
label: "AUD",
tint: "bg-fuchsia-100 text-fuchsia-600 dark:bg-fuchsia-500/15 dark:text-fuchsia-300",
ring: "group-hover:ring-fuchsia-300/70 dark:group-hover:ring-fuchsia-500/40",
glyph: (
<svg viewBox="0 0 24 24" fill="none" className={common} aria-hidden="true">
<path d="M14.75 4.5v10.4a3 3 0 1 1-1.5-2.6V7.4l-5 1.25v8.6a3 3 0 1 1-1.5-2.6V6.5l8-2Z" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round" />
</svg>
),
};
case "code":
return {
label: "SRC",
tint: "bg-zinc-200 text-zinc-700 dark:bg-zinc-500/20 dark:text-zinc-200",
ring: "group-hover:ring-zinc-300 dark:group-hover:ring-zinc-500/50",
glyph: (
<svg viewBox="0 0 24 24" fill="none" className={common} aria-hidden="true">
<path d="M9 8.5 5.5 12 9 15.5M15 8.5 18.5 12 15 15.5M13 6l-2 12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
),
};
}
}
type Status = "idle" | "downloading" | "done";
function FileCard({ file }: { file: FileItem }) {
const style = kindStyle(file.kind);
const [status, setStatus] = useState<Status>("idle");
const [progress, setProgress] = useState(0);
const [starred, setStarred] = useState(false);
const [copied, setCopied] = useState(false);
const reduce = useReducedMotion();
const timers = useRef<number[]>([]);
useEffect(() => {
const list = timers.current;
return () => {
list.forEach((t) => window.clearTimeout(t));
};
}, []);
function push(t: number) {
timers.current.push(t);
}
function startDownload() {
if (status === "downloading") return;
setStatus("downloading");
setProgress(0);
if (reduce) {
setProgress(100);
const t = window.setTimeout(() => setStatus("done"), 400);
push(t);
} else {
const steps = [14, 33, 51, 68, 82, 94, 100];
steps.forEach((value, i) => {
const t = window.setTimeout(() => {
setProgress(value);
if (value === 100) {
const done = window.setTimeout(() => setStatus("done"), 260);
push(done);
}
}, 190 * (i + 1));
push(t);
});
}
const reset = window.setTimeout(() => {
setStatus("idle");
setProgress(0);
}, reduce ? 2200 : 3600);
push(reset);
}
async function copyLink() {
try {
await navigator.clipboard.writeText(`https://files.northwind.app/d/${file.id}/${file.name}`);
setCopied(true);
const t = window.setTimeout(() => setCopied(false), 1600);
push(t);
} catch {
setCopied(false);
}
}
const downloading = status === "downloading";
const done = status === "done";
return (
<div className="group relative flex flex-col rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm transition-all duration-300 hover:-translate-y-0.5 hover:border-zinc-300 hover:shadow-lg dark:border-zinc-800 dark:bg-zinc-900 dark:hover:border-zinc-700 dark:hover:shadow-black/40">
<div className="flex items-start gap-3.5">
<div
className={`flex h-12 w-12 shrink-0 items-center justify-center rounded-xl ring-1 ring-transparent transition-all duration-300 ${style.tint} ${style.ring}`}
>
{style.glyph}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<p className="truncate text-sm font-semibold text-zinc-900 dark:text-zinc-100" title={file.name}>
{file.name}
</p>
<span className="shrink-0 rounded-md bg-zinc-100 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wide text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400">
{style.label}
</span>
</div>
<p className="mt-1 truncate text-xs text-zinc-500 dark:text-zinc-400">
{file.meta} · {formatBytes(file.bytes)}
</p>
</div>
<button
type="button"
onClick={() => setStarred((s) => !s)}
aria-pressed={starred}
aria-label={starred ? `Unstar ${file.name}` : `Star ${file.name}`}
className="-mr-1 -mt-1 rounded-lg p-1.5 text-zinc-400 transition-colors hover:text-amber-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-amber-500/60 dark:hover:text-amber-400"
>
<svg
viewBox="0 0 24 24"
className="h-[1.125rem] w-[1.125rem]"
fill={starred ? "currentColor" : "none"}
stroke="currentColor"
strokeWidth="1.6"
aria-hidden="true"
>
<path d="m12 3.5 2.6 5.27 5.82.85-4.21 4.1.99 5.8L12 17.77l-5.2 2.75.99-5.8-4.21-4.1 5.82-.85L12 3.5Z" strokeLinejoin="round" />
</svg>
</button>
</div>
<div className="mt-4 flex items-center gap-2">
<button
type="button"
onClick={startDownload}
disabled={downloading}
aria-label={`Download ${file.name}`}
className={`relative flex h-9 flex-1 items-center justify-center gap-2 overflow-hidden rounded-lg px-3 text-sm font-semibold transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed dark:focus-visible:ring-offset-zinc-900 ${
done
? "bg-emerald-500 text-white focus-visible:ring-emerald-500/60"
: "bg-indigo-600 text-white hover:bg-indigo-500 focus-visible:ring-indigo-500/60"
}`}
>
{downloading && (
<span
className="absolute inset-y-0 left-0 bg-white/20"
style={{ width: `${progress}%`, transition: reduce ? "none" : "width 180ms ease-out" }}
aria-hidden="true"
/>
)}
<span className="relative flex items-center gap-2">
{done ? (
<>
<svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="2.2" aria-hidden="true">
<path d="m5 12.5 4.5 4.5L19 7" strokeLinecap="round" strokeLinejoin="round" />
</svg>
Downloaded
</>
) : downloading ? (
<>
<svg viewBox="0 0 24 24" className="h-4 w-4 cf-spin" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<path d="M12 3a9 9 0 1 0 9 9" strokeLinecap="round" />
</svg>
{progress}%
</>
) : (
<>
<svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="1.8" aria-hidden="true">
<path d="M12 3.5v11m0 0 3.75-3.75M12 14.5 8.25 10.75M4.5 17.5v1A2 2 0 0 0 6.5 20.5h11a2 2 0 0 0 2-2v-1" strokeLinecap="round" strokeLinejoin="round" />
</svg>
Download
</>
)}
</span>
</button>
<button
type="button"
onClick={copyLink}
aria-label={copied ? "Link copied" : `Copy share link for ${file.name}`}
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-zinc-200 bg-white text-zinc-600 transition-colors hover:bg-zinc-50 hover:text-zinc-900 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500/60 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:bg-zinc-800 dark:hover:text-white"
>
<AnimatePresence mode="wait" initial={false}>
{copied ? (
<motion.svg
key="check"
viewBox="0 0 24 24"
className="h-4 w-4 text-emerald-500"
fill="none"
stroke="currentColor"
strokeWidth="2"
aria-hidden="true"
initial={reduce ? false : { scale: 0.6, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={reduce ? undefined : { scale: 0.6, opacity: 0 }}
transition={{ duration: 0.15 }}
>
<path d="m5 12.5 4.5 4.5L19 7" strokeLinecap="round" strokeLinejoin="round" />
</motion.svg>
) : (
<motion.svg
key="copy"
viewBox="0 0 24 24"
className="h-4 w-4"
fill="none"
stroke="currentColor"
strokeWidth="1.7"
aria-hidden="true"
initial={reduce ? false : { scale: 0.6, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={reduce ? undefined : { scale: 0.6, opacity: 0 }}
transition={{ duration: 0.15 }}
>
<rect x="9" y="9" width="11" height="11" rx="2" />
<path d="M15 5.5V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h.5" />
</motion.svg>
)}
</AnimatePresence>
</button>
</div>
</div>
);
}
export default function CardFile() {
return (
<section className="relative w-full bg-zinc-50 px-4 py-16 sm:px-6 sm:py-24 dark:bg-zinc-950">
<style>{`
@keyframes cardfile_spin { to { transform: rotate(360deg); } }
.cf-spin { animation: cardfile_spin 0.8s linear infinite; transform-origin: center; }
@media (prefers-reduced-motion: reduce) {
.cf-spin { animation: none; }
}
`}</style>
<div className="mx-auto max-w-5xl">
<div className="mb-10 text-center">
<span className="inline-flex items-center rounded-full border border-zinc-200 bg-white px-3 py-1 text-xs font-medium text-zinc-500 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-400">
File library
</span>
<h2 className="mt-4 text-2xl font-bold tracking-tight text-zinc-900 sm:text-3xl dark:text-zinc-50">
Shared files
</h2>
<p className="mx-auto mt-2 max-w-md text-sm text-zinc-500 dark:text-zinc-400">
Type-aware cards with size, star, copy link, and a real download flow.
</p>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-2">
{FILES.map((file) => (
<FileCard key={file.id} file={file} />
))}
</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 →
3D Tilt Cards
OriginalA responsive grid of cards that tilt in 3D toward your cursor with a light glare and lifted depth layers, powered by Framer Motion springs.

Spotlight Follow Cards
OriginalFeature cards where a soft radial spotlight and a glowing border chase the cursor on hover, with a staggered scroll reveal and an animated shimmer line.

3D Flip Cards
OriginalPricing-style cards that flip in 3D on hover or tap to reveal details on the back, keyboard accessible with a staggered entrance animation.

Animated Gradient Border Cards
OriginalCards wrapped in a live conic gradient border that rotates and speeds up on hover, with a shimmer sweep and a scrolling tag marquee.

Glow Effect Card
primitivesA reusable glow effect card for React and Tailwind, with hover and motion.

Neon Gradient Card
gradient-card.tsxA reusable neon gradient card for React and Tailwind, with hover and motion.

Spotlight Magic Card
card.tsxA reusable spotlight magic card for React and Tailwind, with hover and motion.

Tilt Spotlight Card
primitivesA reusable tilt spotlight card for React and Tailwind, with hover and motion.

Profile Card
OriginalA user profile card with avatar, stats and a follow toggle.

Product Card
OriginalA product card with image, price and add-to-cart feedback.

Pricing Single Card
OriginalA single pricing plan card with feature list and CTA.

Stat Card
OriginalA metric card with value, trend arrow and a small sparkline.

