Copy field
- Source
- After bencho.dev ↗
- Author
- Interaction after bencho.dev
- Licence
- Original code — interaction after bencho.dev
Loading…
Click copy to put the command on your clipboard, with a little confirmation.
Code
"use client";
import { AnimatePresence, motion } from "motion/react";
import { Check, Copy } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { Surface } from "../ui";
const VALUE = "npm install motion";
export default function CopyField() {
const [copied, setCopied] = useState(0);
const timer = useRef(0);
useEffect(() => () => window.clearTimeout(timer.current), []);
const copy = async () => {
try {
await navigator.clipboard?.writeText(VALUE);
} catch {
/* the visual feedback still plays */
}
setCopied((c) => c + 1);
window.clearTimeout(timer.current);
timer.current = window.setTimeout(() => setCopied(0), 1800);
};
const on = copied > 0;
return (
<Surface>
<div className="relative w-[20em]">
<p className="mb-[0.5em] text-[0.75em] text-[#8a8984]">Install</p>
<div className="relative flex h-[3em] items-center overflow-hidden rounded-[0.8em] bg-white pl-[1em] pr-[0.4em] shadow-[0_6px_20px_-14px_rgba(0,0,0,0.3),0_0_0_1px_rgba(0,0,0,0.05)]">
<span className="mr-[0.6em] font-mono text-[0.85em] text-[#c9c8c3]">$</span>
<span className="relative flex-1 font-mono text-[0.85em]">
{VALUE}
{/* a selection sweep when copied */}
<AnimatePresence>
{on && (
<motion.span
key={copied}
className="absolute inset-y-[-0.15em] left-[-0.2em] rounded-[0.2em] bg-[#2B46B8]/15"
initial={{ width: 0, opacity: 1 }}
animate={{ width: "calc(100% + 0.4em)" }}
exit={{ opacity: 0 }}
transition={{ duration: 0.35, ease: "easeOut" }}
/>
)}
</AnimatePresence>
</span>
<motion.button
type="button"
aria-label="Copy"
onClick={copy}
whileTap={{ scale: 0.85 }}
className={`relative grid h-[2.2em] w-[2.2em] place-items-center rounded-[0.6em] transition-colors ${on ? "bg-[#151515] text-white" : "bg-[#f1f0ec] text-[#151515] hover:bg-[#e4e3de]"}`}
>
<AnimatePresence mode="popLayout" initial={false}>
<motion.span
key={on ? "check" : "copy"}
initial={{ scale: 0.3, opacity: 0, rotate: on ? -45 : 45 }}
animate={{ scale: 1, opacity: 1, rotate: 0 }}
exit={{ scale: 0.3, opacity: 0 }}
transition={{ type: "spring", stiffness: 500, damping: 25 }}
>
{on ? <Check className="h-[1em] w-[1em]" strokeWidth={2.6} /> : <Copy className="h-[1em] w-[1em]" />}
</motion.span>
</AnimatePresence>
</motion.button>
</div>
<div className="absolute -top-[0.6em] right-0">
<AnimatePresence>
{on && (
<motion.span
initial={{ opacity: 0, y: 8, scale: 0.9 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -6 }}
className="block rounded-full bg-[#151515] px-[0.7em] py-[0.25em] text-[0.7em] text-white"
>
Copied
</motion.span>
)}
</AnimatePresence>
</div>
</div>
</Surface>
);
}
Prompt
Build "Copy field", a input animation. What it does: Click copy to put the command on your clipboard, with a little confirmation. 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).