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).