Baktiyar Ablaikhan

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