feat: redesign drop zone as full-bleed poster typography
Replace card/icon layout with a massive stacked serif headline that fills the window. Words animate in individually and shift pink on drag. Browse button is small and understated below the type. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+43
-113
@@ -15,16 +15,7 @@ interface DropZoneProps {
|
|||||||
onBrowse: () => void;
|
onBrowse: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FORMAT_PILLS = [
|
const WORDS = ["Drop", "files", "here"];
|
||||||
{ label: "IMG", color: "bg-pink/10 text-pink" },
|
|
||||||
{ label: "PDF", color: "bg-orange/10 text-orange" },
|
|
||||||
{ label: "MP4", color: "bg-purple/10 text-purple" },
|
|
||||||
{ label: "MP3", color: "bg-blue/10 text-blue" },
|
|
||||||
{ label: "SVG", color: "bg-teal/10 text-teal" },
|
|
||||||
{ label: "CSV", color: "bg-mint/10 text-mint" },
|
|
||||||
{ label: "DOCX", color: "bg-orange/10 text-orange" },
|
|
||||||
{ label: "+60", color: "bg-[#f6f6f6] text-text-light" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export function DropZone({
|
export function DropZone({
|
||||||
isDragging,
|
isDragging,
|
||||||
@@ -36,120 +27,59 @@ export function DropZone({
|
|||||||
}: DropZoneProps) {
|
}: DropZoneProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex min-h-full items-center justify-center px-6 sm:px-10 py-12 sm:py-20"
|
className="flex min-h-full w-full flex-col items-center justify-center px-6 py-10 select-none"
|
||||||
style={{ minHeight: "100%" }}
|
style={{ minHeight: "100%" }}
|
||||||
onDragEnter={onDragEnter}
|
onDragEnter={onDragEnter}
|
||||||
onDragLeave={onDragLeave}
|
onDragLeave={onDragLeave}
|
||||||
onDragOver={onDragOver}
|
onDragOver={onDragOver}
|
||||||
onDrop={onDrop}
|
onDrop={onDrop}
|
||||||
>
|
>
|
||||||
<motion.div
|
{/* Poster headline */}
|
||||||
className="flex w-full max-w-lg flex-col items-center text-center"
|
<div className="flex flex-col items-center leading-none mb-10">
|
||||||
initial={{ opacity: 0, y: 16 }}
|
{WORDS.map((word, i) => (
|
||||||
animate={{ opacity: 1, y: 0 }}
|
<motion.span
|
||||||
transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1] as const }}
|
key={word}
|
||||||
>
|
className="font-serif font-extrabold tracking-tight block"
|
||||||
{/* Card */}
|
style={{ fontSize: "clamp(3.5rem, 16vw, 14rem)" }}
|
||||||
<div
|
initial={{ opacity: 0, y: 24 }}
|
||||||
className={`w-full flex flex-col items-center gap-5 sm:gap-6 px-8 sm:px-12 py-10 sm:py-12 rounded-2xl border transition-all duration-300 ${
|
|
||||||
isDragging
|
|
||||||
? "bg-pink/[0.03] border-pink/20 shadow-[0_8px_40px_rgba(244,114,182,0.12)]"
|
|
||||||
: "bg-white border-border-soft shadow-[0_4px_24px_rgba(45,31,20,0.07)]"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{/* Upload icon — gradient bg + glow */}
|
|
||||||
<motion.div
|
|
||||||
className={`flex items-center justify-center w-20 h-20 sm:w-24 sm:h-24 rounded-2xl transition-all duration-300 ${
|
|
||||||
isDragging
|
|
||||||
? "shadow-[0_0_40px_rgba(244,114,182,0.4)]"
|
|
||||||
: "shadow-[0_0_32px_rgba(244,114,182,0.2)]"
|
|
||||||
}`}
|
|
||||||
style={{
|
|
||||||
background: isDragging
|
|
||||||
? "linear-gradient(135deg, #f472b6 0%, #a78bfa 100%)"
|
|
||||||
: "linear-gradient(135deg, #f9a8d4 0%, #c4b5fd 100%)",
|
|
||||||
}}
|
|
||||||
animate={{
|
animate={{
|
||||||
y: isDragging ? -6 : 0,
|
opacity: 1,
|
||||||
rotate: isDragging ? -3 : 0,
|
y: 0,
|
||||||
|
color: isDragging ? "#f472b6" : "#2d1f14",
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
opacity: { duration: 0.5, delay: i * 0.07, ease: [0.16, 1, 0.3, 1] },
|
||||||
|
y: { duration: 0.5, delay: i * 0.07, ease: [0.16, 1, 0.3, 1] },
|
||||||
|
color: { duration: 0.3 },
|
||||||
}}
|
}}
|
||||||
transition={{ type: "spring", stiffness: 300, damping: 20 }}
|
|
||||||
>
|
>
|
||||||
<svg
|
{word}
|
||||||
width="36"
|
</motion.span>
|
||||||
height="36"
|
))}
|
||||||
viewBox="0 0 48 48"
|
</div>
|
||||||
fill="none"
|
|
||||||
className="sm:w-10 sm:h-10"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M24 32V12M24 12L16 20M24 12L32 20"
|
|
||||||
stroke="white"
|
|
||||||
strokeWidth="2.5"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M8 28v8a4 4 0 004 4h24a4 4 0 004-4v-8"
|
|
||||||
stroke="white"
|
|
||||||
strokeWidth="2.5"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Heading + subtitle */}
|
{/* Browse button — small and understated */}
|
||||||
<div>
|
<motion.div
|
||||||
<h2 className="font-serif text-3xl sm:text-4xl font-extrabold text-text-dark tracking-tight mb-2">
|
initial={{ opacity: 0, y: 8 }}
|
||||||
{isDragging ? "Release to add" : "Drop files here"}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
</h2>
|
transition={{ duration: 0.4, delay: 0.28, ease: [0.16, 1, 0.3, 1] }}
|
||||||
<p className="text-text-mid text-sm sm:text-base leading-relaxed">
|
className="flex flex-col items-center gap-4"
|
||||||
{isDragging
|
>
|
||||||
? "Your files are ready for transformation"
|
<motion.button
|
||||||
: "Images, documents, audio, video, data — all formats welcome"}
|
className="inline-flex items-center gap-2 px-5 py-2 text-sm font-semibold text-white bg-pink rounded-xl cursor-pointer border-none shadow-[0_4px_16px_rgba(244,114,182,0.3)] hover:shadow-[0_6px_24px_rgba(244,114,182,0.4)] transition-shadow"
|
||||||
</p>
|
onClick={onBrowse}
|
||||||
</div>
|
whileHover={{ scale: 1.04 }}
|
||||||
|
whileTap={{ scale: 0.96 }}
|
||||||
|
>
|
||||||
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
|
<path d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
||||||
|
</svg>
|
||||||
|
Browse files
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
{/* Format pills */}
|
<p className="font-mono text-[10px] text-text-light/50 tracking-wide">
|
||||||
{!isDragging && (
|
70+ formats — 100% client-side
|
||||||
<div className="flex flex-wrap justify-center gap-1.5">
|
</p>
|
||||||
{FORMAT_PILLS.map(({ label, color }) => (
|
|
||||||
<span
|
|
||||||
key={label}
|
|
||||||
className={`font-mono text-[10px] font-semibold tracking-wide px-2.5 py-1 rounded-full ${color}`}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Browse button */}
|
|
||||||
<motion.button
|
|
||||||
className="inline-flex items-center gap-2.5 px-7 sm:px-8 py-3 sm:py-3.5 text-base sm:text-lg font-bold text-white bg-pink rounded-2xl cursor-pointer shadow-[0_6px_24px_rgba(244,114,182,0.25)] hover:-translate-y-0.5 hover:shadow-[0_8px_30px_rgba(244,114,182,0.35)] active:scale-[0.97] transition-all border-none"
|
|
||||||
onClick={onBrowse}
|
|
||||||
whileHover={{ scale: 1.03 }}
|
|
||||||
whileTap={{ scale: 0.97 }}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="14"
|
|
||||||
height="14"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<path d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
|
||||||
</svg>
|
|
||||||
Browse files
|
|
||||||
</motion.button>
|
|
||||||
|
|
||||||
{/* Trust signal */}
|
|
||||||
<p className="font-mono text-[11px] text-text-light/60 tracking-wide -mt-1">
|
|
||||||
70+ formats — 100% client-side
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user