Baktiyar Ablaikhan

Tape

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

Loading…

Pull the tip out of the tape measure; it snaps back unless you lock it.

Code

"use client";

import { animate, motion, useMotionValue, useMotionValueEvent, useTransform } from "motion/react";
import { Lock, LockOpen } from "lucide-react";
import { useRef, useState } from "react";
import { Surface } from "../ui";

const CM = 1.4; // em per centimetre

export default function Tape() {
  const x = useMotionValue(0);
  const region = useRef<HTMLDivElement>(null);
  const [reading, setReading] = useState(0);
  const [locked, setLocked] = useState(false);
  const width = useTransform(x, (v) => Math.max(0, v));

  useMotionValueEvent(x, "change", (v) => {
    const fs = region.current ? parseFloat(getComputedStyle(region.current).fontSize) : 16;
    setReading(Math.max(0, v / (fs * CM)));
  });

  const retract = () => animate(x, 0, { type: "spring", stiffness: 160, damping: 11, mass: 0.8 });

  return (
    <Surface>
      <div className="flex flex-col items-center gap-[1.4em]">
        <div className="font-mono tabular-nums">
          <span className="text-[2.4em] leading-none">{reading.toFixed(1)}</span>
          <span className="ml-[0.3em] text-[0.9em] text-[#8a8984]">cm</span>
        </div>
        <div className="flex items-center">
          {/* the case */}
          <div className="relative z-10 grid h-[5.6em] w-[5.6em] place-items-center rounded-[1.4em] bg-[#151515] shadow-[0_14px_30px_-14px_rgba(0,0,0,0.6)]">
            <div className="h-[3.2em] w-[3.2em] rounded-full border-[0.35em] border-[#2b2b2b] bg-[#1f1f1f]" />
            <button
              type="button"
              aria-label={locked ? "Unlock tape" : "Lock tape"}
              onClick={() => {
                if (locked) retract();
                setLocked(!locked);
              }}
              className={`absolute -top-[0.9em] left-1/2 grid h-[1.8em] w-[1.8em] -translate-x-1/2 place-items-center rounded-full border-[0.15em] border-[#f1f0ec] ${locked ? "bg-[#2B46B8] text-white" : "bg-white text-[#151515]"}`}
            >
              {locked ? <Lock className="h-[0.8em] w-[0.8em]" /> : <LockOpen className="h-[0.8em] w-[0.8em]" />}
            </button>
          </div>
          <div ref={region} className="relative h-[5.6em] w-[17em]">
            <motion.div
              style={{ width }}
              className="absolute left-0 top-1/2 h-[1.7em] -translate-y-[10%] overflow-hidden bg-white shadow-[0_2px_6px_-2px_rgba(0,0,0,0.2)]"
            >
              {/* ruler content anchored at the tip, so numbers travel with it */}
              <div className="absolute right-0 top-0 h-full w-[40em]">
                <div className="absolute inset-x-0 top-0 h-[38%] bg-[repeating-linear-gradient(270deg,#151515_0_1px,transparent_1px_0.28em)]" />
                <div className="absolute inset-x-0 top-0 h-[60%] bg-[repeating-linear-gradient(270deg,#151515_0_1.5px,transparent_1.5px_1.4em)]" />
                {Array.from({ length: 28 }, (_, i) => (
                  <span key={i} className="absolute bottom-[0.1em] font-mono text-[0.55em] text-[#151515]" style={{ right: `${(i * CM) / 0.55 + 0.25}em` }}>
                    {i}
                  </span>
                ))}
              </div>
            </motion.div>
            <motion.div
              drag="x"
              dragConstraints={region}
              dragElastic={0.04}
              dragMomentum={false}
              onDragEnd={() => {
                if (!locked) retract();
              }}
              style={{ x }}
              whileDrag={{ scale: 1.06 }}
              className="absolute left-0 top-1/2 -ml-[0.2em] h-[2.6em] w-[1.1em] -translate-y-[38%] cursor-grab touch-none rounded-[0.3em] bg-[#2B46B8] shadow-[0_4px_10px_-4px_rgba(43,70,184,0.7)] active:cursor-grabbing"
            >
              <span className="absolute inset-y-[0.5em] left-1/2 w-[2px] -translate-x-1/2 rounded bg-white/50" />
            </motion.div>
          </div>
        </div>
        <p className="font-mono text-[0.7em] text-[#8a8984]">pull the tip · lock to hold</p>
      </div>
    </Surface>
  );
}
Prompt

Build "Tape", a input animation. What it does: Pull the tip out of the tape measure; it snaps back unless you lock it. Write it as a single self-contained React component in TypeScript that fills its parent (w-full h-full), styled with Tailwind CSS, using lucide-react, 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).