{
    "id": "bhaskar/copy-morph-button",
    "kind": "component",
    "title": "Copy Morph Button",
    "summary": "A copy button whose icon morphs into a check and says Copied, then resets.",
    "author": "Admin",
    "stars": 0,
    "url": "https://ownlyplans.com/bhaskar/copy-morph-button/",
    "stack": [],
    "tools": [
        "claude-code",
        "cursor",
        "lovable"
    ],
    "difficulty": "beginner",
    "license": "mit",
    "version": "1.0",
    "overview": "The two icons cross-fade with a small scale so the change is clear without a toast. The label update is announced to screen readers.",
    "steps": [],
    "files": [],
    "install": "npx shadcn@latest add https://ownlyplans.com/r/bhaskar/copy-morph-button.json",
    "how_to_use": "Use the component code (html, css, js, or react) directly, or run the install command.",
    "component": {
        "category": "Buttons",
        "html": "<button class=\"cp\" type=\"button\" data-text=\"npm i ownlyplans\"><span class=\"cp__i\"><svg class=\"cp__copy\" viewBox=\"0 0 24 24\"><rect x=\"9\" y=\"9\" width=\"11\" height=\"11\" rx=\"2\"/><path d=\"M5 15V6a2 2 0 0 1 2-2h9\"/></svg><svg class=\"cp__ok\" viewBox=\"0 0 24 24\"><path d=\"m5 12 5 5 9-10\"/></svg></span><span class=\"cp__t\" aria-live=\"polite\">Copy</span></button>",
        "css": ".cp{display:inline-flex;align-items:center;gap:10px;padding:12px 18px;border:1px solid var(--line);border-radius:12px;background:transparent;color:var(--ink);font:600 15px/1 system-ui,sans-serif;cursor:pointer}\n.cp:focus-visible{outline:2px solid var(--accent);outline-offset:3px}\n.cp__i{position:relative;width:18px;height:18px}\n.cp svg{position:absolute;inset:0;width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;transition:opacity .25s,transform .35s cubic-bezier(.2,.8,.2,1)}\n.cp__ok{opacity:0;transform:scale(.4);stroke:#16a34a}\n.cp.is-done .cp__copy{opacity:0;transform:scale(.4)}\n.cp.is-done .cp__ok{opacity:1;transform:none}",
        "js": "const c=document.querySelector('.cp');const t=c.querySelector('.cp__t');let timer;\nc.addEventListener('click',()=>{try{navigator.clipboard.writeText(c.dataset.text).catch(()=>{})}catch(e){}\nc.classList.add('is-done');t.textContent='Copied';clearTimeout(timer);timer=setTimeout(()=>{c.classList.remove('is-done');t.textContent='Copy'},1600)});",
        "react": "\"use client\";\nimport { useEffect, useRef, useState } from \"react\";\n\nconst css = `\n.cp{display:inline-flex;align-items:center;gap:10px;padding:12px 18px;border:1px solid #d8deeb;border-radius:12px;background:transparent;color:inherit;font:600 15px/1 system-ui,sans-serif;cursor:pointer}\n.cp:focus-visible{outline:2px solid #1659f5;outline-offset:3px}\n.cp__i{position:relative;width:18px;height:18px}\n.cp svg{position:absolute;inset:0;width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;transition:opacity .25s,transform .35s cubic-bezier(.2,.8,.2,1)}\n.cp__ok{opacity:0;transform:scale(.4);stroke:#16a34a}\n.cp.is-done .cp__copy{opacity:0;transform:scale(.4)}\n.cp.is-done .cp__ok{opacity:1;transform:none}`;\n\nexport function CopyMorphButton({ text, label = \"Copy\", done = \"Copied\" }: { text: string; label?: string; done?: string }) {\n  const [copied, setCopied] = useState(false);\n  const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n  useEffect(() => () => clearTimeout(timer.current), []);\n  const copy = async () => {\n    try { await navigator.clipboard.writeText(text); } catch {}\n    setCopied(true);\n    clearTimeout(timer.current);\n    timer.current = setTimeout(() => setCopied(false), 1600);\n  };\n  return (\n    <>\n      <style>{css}</style>\n      <button type=\"button\" className={`cp${copied ? \" is-done\" : \"\"}`} onClick={copy}>\n        <span className=\"cp__i\" aria-hidden=\"true\">\n          <svg className=\"cp__copy\" viewBox=\"0 0 24 24\"><rect x=\"9\" y=\"9\" width=\"11\" height=\"11\" rx=\"2\" /><path d=\"M5 15V6a2 2 0 0 1 2-2h9\" /></svg>\n          <svg className=\"cp__ok\" viewBox=\"0 0 24 24\"><path d=\"m5 12 5 5 9-10\" /></svg>\n        </span>\n        <span aria-live=\"polite\">{copied ? done : label}</span>\n      </button>\n    </>\n  );\n}\n\nexport default CopyMorphButton;",
        "dependencies": [],
        "install": "npx shadcn@latest add https://ownlyplans.com/r/bhaskar/copy-morph-button.json",
        "licence_notice": ""
    }
}