Baktiyar Ablaikhan

Coin

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

Loading…

Click the coin to flip it and keep a tally of heads and tails.

Code

"use client";

import { animate, motion, useMotionValue, useTransform } from "motion/react";
import { useRef, useState } from "react";
import { Surface } from "../ui";

const LAYERS = 7;

function Face({ back }: { back?: boolean }) {
  return (
    <div
      className="absolute inset-0 grid place-items-center rounded-full [backface-visibility:hidden]"
      style={{
        transform: back ? `rotateX(180deg) translateZ(${LAYERS / 2}px)` : `translateZ(${LAYERS / 2}px)`,
        background: "radial-gradient(circle at 35% 30%, #fdfdfb 0, #e3e2dc 45%, #b9b8b1 100%)",
        boxShadow: "inset 0 0 0 0.35em #d2d1cb, inset 0 0 0 0.45em #f7f6f2, inset 0 0 0 0.9em #dddcd6",
      }}
    >
      {back ? (
        <svg viewBox="0 0 24 24" className="h-[2.6em] w-[2.6em] text-[#8a8984]" fill="currentColor">
          <path d="m12 2.8 2.7 5.6 6.1.9-4.4 4.3 1 6.1L12 16.8l-5.4 2.9 1-6.1-4.4-4.3 6.1-.9z" />
        </svg>
      ) : (
        <span className="font-serif text-[3em] leading-none text-[#77766f] [text-shadow:0_1px_0_#fff,0_-1px_0_rgba(0,0,0,0.15)]">B</span>
      )}
    </div>
  );
}

export default function Coin() {
  const rot = useMotionValue(0);
  const lift = useMotionValue(0);
  const [tally, setTally] = useState({ heads: 0, tails: 0 });
  const [last, setLast] = useState<"heads" | "tails" | null>(null);
  const busy = useRef(false);
  const shadowScale = useTransform(lift, [-110, 0], [0.45, 1]);
  const shadowOpacity = useTransform(lift, [-110, 0], [0.08, 0.22]);

  const flip = async (e: React.MouseEvent<HTMLButtonElement>) => {
    const peak = -e.currentTarget.getBoundingClientRect().height * 0.85;
    if (busy.current) return;
    busy.current = true;
    setLast(null);
    const heads = Math.random() < 0.5;
    const base = Math.ceil(rot.get() / 360) * 360;
    const target = base + 360 * 4 + (heads ? 0 : 180);
    animate(lift, [0, peak, 0], { duration: 1.05, times: [0, 0.45, 1], ease: ["easeOut", "easeIn"] });
    await animate(rot, target, { duration: 1.05, ease: [0.25, 0.6, 0.35, 1] });
    await animate(lift, [0, -14, 0], { duration: 0.28, ease: "easeOut" });
    const side = heads ? "heads" : "tails";
    setLast(side);
    setTally((t) => ({ ...t, [side]: t[side] + 1 }));
    busy.current = false;
  };

  return (
    <Surface>
      <div className="relative flex translate-y-[1.6em] flex-col items-center">
        <motion.button
          type="button"
          aria-label="Flip the coin"
          onClick={flip}
          whileHover={{ scale: 1.04 }}
          whileTap={{ scale: 0.95 }}
          style={{ y: lift }}
          className="relative h-[7em] w-[7em] cursor-pointer [perspective:600px]"
        >
          <motion.div className="absolute inset-0 [transform-style:preserve-3d]" style={{ rotateX: rot }}>
            {Array.from({ length: LAYERS }, (_, i) => (
              <div
                key={i}
                className="absolute inset-0 rounded-full bg-[#a9a8a1]"
                style={{ transform: `translateZ(${i - LAYERS / 2 + 0.5}px)` }}
              />
            ))}
            <Face />
            <Face back />
          </motion.div>
        </motion.button>
        <motion.div
          className="mt-[1.4em] h-[0.9em] w-[6em] rounded-[50%] bg-black blur-[4px]"
          style={{ scale: shadowScale, opacity: shadowOpacity }}
        />
        <div className="mt-[1.2em] flex items-center gap-[1em] font-mono text-[0.72em] text-[#8a8984]">
          <span className={last === "heads" ? "text-[#151515]" : ""}>heads {tally.heads}</span>
          <span className="h-[1em] w-px bg-[#c9c8c3]" />
          <span className={last === "tails" ? "text-[#151515]" : ""}>tails {tally.tails}</span>
        </div>
      </div>
    </Surface>
  );
}
Prompt

Build "Coin", a button animation. What it does: Click the coin to flip it and keep a tally of heads and tails. 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, inline SVG, 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).