Baktiyar Ablaikhan

Folder

Source
After bencho.dev ↗
Author
Interaction after bencho.dev
Licence
Original code — interaction after bencho.dev

Loading…

Hover the folder to peek inside; click to fan the files out.

Code

"use client";

import { motion } from "motion/react";
import { useState } from "react";
import { Surface } from "../ui";

const FILES = [
  { name: "brief.pdf", tint: "#ffffff", line: "#e4e3de" },
  { name: "moodboard.png", tint: "#e8ecfb", line: "#b9c4f0" },
  { name: "notes.md", tint: "#fbfaf7", line: "#e4e3de" },
];

export default function Folder() {
  const [hover, setHover] = useState(false);
  const [open, setOpen] = useState(false);
  const lift = open ? 2 : hover ? 1 : 0;

  return (
    <Surface>
      <div className="flex flex-col items-center gap-[1.2em]">
        <button
          type="button"
          onPointerEnter={() => setHover(true)}
          onPointerLeave={() => setHover(false)}
          onClick={() => setOpen((o) => !o)}
          className="relative h-[9.5em] w-[12em] cursor-pointer [perspective:700px]"
          aria-label={open ? "Close folder" : "Open folder"}
        >
          {/* back panel with tab */}
          <div className="absolute inset-x-0 bottom-0 h-[8.4em] rounded-[0.9em] rounded-tl-none bg-[#c8cfe8]" />
          <div className="absolute left-0 top-[0.4em] h-[1.4em] w-[4.6em] rounded-t-[0.7em] bg-[#c8cfe8]" />
          {/* papers */}
          {FILES.map((f, i) => {
            const fan = [-16, 0, 16][i];
            return (
              <motion.div
                key={f.name}
                className="absolute bottom-[1.2em] left-1/2 -ml-[4.4em] h-[7em] w-[8.8em] origin-bottom rounded-[0.5em] p-[0.7em] text-left shadow-[0_4px_12px_-6px_rgba(0,0,0,0.3)]"
                style={{ background: f.tint }}
                animate={{
                  y: lift === 2 ? "-4.6em" : lift === 1 ? `${-0.9 - i * 0.35}em` : "0em",
                  x: lift === 2 ? `${fan * 0.24}em` : `${(i - 1) * 0.3}em`,
                  rotate: lift === 2 ? fan : (i - 1) * 2,
                }}
                transition={{ type: "spring", stiffness: 300, damping: 22, delay: lift ? i * 0.04 : (2 - i) * 0.03 }}
              >
                <p className="truncate font-mono text-[0.55em] text-[#8a8984]">{f.name}</p>
                {[80, 95, 70, 88].map((w, k) => (
                  <div key={k} className="mt-[0.45em] h-[0.25em] rounded-full" style={{ width: `${w}%`, background: f.line }} />
                ))}
              </motion.div>
            );
          })}
          {/* front panel */}
          <motion.div
            className="absolute inset-x-0 bottom-0 h-[6.6em] origin-bottom rounded-[0.9em] bg-gradient-to-b from-[#dfe4f5] to-[#cdd4ee] shadow-[0_-1px_0_rgba(255,255,255,0.8)_inset,0_14px_30px_-16px_rgba(43,70,184,0.45)]"
            animate={{ rotateX: lift === 2 ? -38 : lift === 1 ? -18 : 0 }}
            transition={{ type: "spring", stiffness: 260, damping: 20 }}
          >
            <span className="absolute bottom-[0.8em] left-[1em] text-[0.8em] font-medium text-[#2B46B8]">Project files</span>
            <span className="absolute bottom-[0.8em] right-[1em] font-mono text-[0.7em] text-[#2B46B8]/70">3</span>
          </motion.div>
        </button>
        <p className="font-mono text-[0.7em] text-[#8a8984]">{open ? "click to tidy up" : "hover to peek · click to open"}</p>
      </div>
    </Surface>
  );
}
Prompt

Build "Folder", a card animation. What it does: Hover the folder to peek inside; click to fan the files out. Write it as a single self-contained React component in TypeScript that fills its parent (w-full h-full), styled with Tailwind CSS, using motion. Technique: an HTML canvas, the Motion library, pointer tracking. It sits on a light background, in a 16:10 frame. Respect prefers-reduced-motion with a still state, pause work when the element is off screen, and keep it accessible. Original: https://bencho.dev (Interaction after bencho.dev, Original code — interaction after bencho.dev).