npm i @aimran/ui
React + MotionMikrointeraktionen

Befehl-kopieren-Button

Beim Kopieren wischt eine Markierung über den Befehl, und das Icon wird zu einem Häkchen, das sich selbst zeichnet.

Einstellungen

Code

import { useEffect, useRef, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
import { Copy } from "lucide-react";

interface CopyCommandProps {
  command?: string;
  accent?: string;
}

export default function CopyCommand({
  command = "npm i @aimran/ui",
  accent = "#22c55e",
}: CopyCommandProps) {
  const [copied, setCopied] = useState(false);
  const [sweep, setSweep] = useState(0);
  const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
  const reduce = useReducedMotion();

  useEffect(() => () => clearTimeout(timer.current), []);

  const copy = () => {
    try {
      navigator.clipboard?.writeText(command).catch(() => {});
    } catch {
      // Sin portapapeles disponible: la animación sigue funcionando
    }
    setCopied(true);
    setSweep((s) => s + 1);
    clearTimeout(timer.current);
    timer.current = setTimeout(() => setCopied(false), 1800);
  };

  return (
    <div className="flex w-full max-w-[19rem] items-center gap-3 rounded-2xl bg-white py-1.5 pl-4 pr-1.5 font-mono text-[13px] text-[#1d1d1f] shadow-[0_0_0_1px_rgba(0,0,0,0.06),0_1px_2px_rgba(0,0,0,0.04),0_8px_24px_rgba(0,0,0,0.06)] dark:bg-[#1c1c1e] dark:text-[#f5f5f7] dark:shadow-[0_0_0_1px_rgba(255,255,255,0.08),0_8px_24px_rgba(0,0,0,0.4)]">
      <span className="select-none text-[#86868b]" aria-hidden="true">
        $
      </span>
      <span className="relative min-w-0 flex-1">
        {sweep > 0 && !reduce && (
          <motion.span
            key={sweep}
            aria-hidden="true"
            className="absolute -inset-x-1 -inset-y-0.5 origin-left rounded-[5px]"
            style={{ backgroundColor: accent }}
            initial={{ scaleX: 0, opacity: 0.24 }}
            animate={{ scaleX: 1, opacity: [0.24, 0.24, 0] }}
            transition={{
              scaleX: { duration: 0.34, ease: [0.22, 1, 0.36, 1] },
              opacity: { duration: 1.1, times: [0, 0.55, 1] },
            }}
          />
        )}
        <span className="relative block truncate">{command}</span>
      </span>
      <motion.button
        type="button"
        onClick={copy}
        aria-label={copied ? "Kopiert" : "Befehl kopieren"}
        whileTap={{ scale: 0.88 }}
        transition={{ type: "spring", stiffness: 600, damping: 30 }}
        className="relative grid h-9 w-9 shrink-0 place-items-center rounded-xl text-[#86868b] outline-none transition-colors hover:bg-black/5 hover:text-[#1d1d1f] focus-visible:ring-2 focus-visible:ring-black/20 dark:hover:bg-white/10 dark:hover:text-[#f5f5f7] dark:focus-visible:ring-white/30"
      >
        <motion.span
          aria-hidden="true"
          className="absolute inset-0 rounded-xl"
          style={{ backgroundColor: accent }}
          initial={false}
          animate={{ opacity: copied ? 0.14 : 0 }}
          transition={{ duration: 0.2 }}
        />
        <AnimatePresence mode="popLayout" initial={false}>
          {copied ? (
            <motion.svg
              key="check"
              viewBox="0 0 24 24"
              className="relative h-[18px] w-[18px]"
              fill="none"
              stroke={accent}
              strokeWidth={2.25}
              strokeLinecap="round"
              strokeLinejoin="round"
              initial={{ opacity: 0, scale: 0.5 }}
              animate={{ opacity: 1, scale: 1 }}
              exit={{ opacity: 0, scale: 0.5, filter: "blur(2px)" }}
              transition={{ type: "spring", stiffness: 500, damping: 26 }}
            >
              <motion.path
                d="M5 12.5l4.5 4.5L19 7.5"
                initial={{ pathLength: 0 }}
                animate={{ pathLength: 1 }}
                transition={{ duration: 0.3, delay: 0.05, ease: [0.65, 0, 0.35, 1] }}
              />
            </motion.svg>
          ) : (
            <motion.span
              key="copy"
              className="relative grid place-items-center"
              initial={{ opacity: 0, scale: 0.5, filter: "blur(2px)" }}
              animate={{ opacity: 1, scale: 1, filter: "blur(0px)" }}
              exit={{ opacity: 0, scale: 0.5, filter: "blur(2px)" }}
              transition={{ type: "spring", stiffness: 500, damping: 30 }}
            >
              <Copy className="h-[18px] w-[18px]" strokeWidth={1.5} />
            </motion.span>
          )}
        </AnimatePresence>
      </motion.button>
      <span className="sr-only" aria-live="polite">
        {copied ? "In die Zwischenablage kopiert" : ""}
      </span>
    </div>
  );
}

Prompt für KI

Füg ihn in ChatGPT, Claude oder Cursor ein und der Baustein wird mit diesen Einstellungen an dein Projekt angepasst, auch ohne React.

Mehr aus Mikrointeraktionen

Wofür du ihn nutzen kannst

Ein Button zum Kopieren in die Zwischenablage gehört zu den Details, die erst auffallen, wenn sie fehlen. Er passt neben den Installationsbefehl in deiner Doku, einen API-Schlüssel im Dashboard, einen Rabattcode oder einen Einladungslink. Die Markierung zeigt genau, was kopiert wurde, und Screenreader sagen „Kopiert“ an.

Browser erlauben das Kopieren nur auf HTTPS-Seiten; unter reinem HTTP läuft die Animation, kopiert wird aber nichts. Lange Befehle werden mit Auslassungspunkten gekürzt; ist deiner lang, gib der Box mehr Breite. Das grüne Häkchen versteht fast jeder als Erfolg.