{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "copy-button",
  "title": "Copy button",
  "description": "Copies a value, and the icon and label roll over to say it worked, or that the browser said no.",
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/manniche/copy-button/copy-button.tsx",
      "content": "// Based on Watermelon UI's “Copy confirm” (MIT, © 2026 Watermelon Platform Contributors,\n// github.com/WatermelonCorp/watermelon-platform). Rewritten to handle a refused clipboard.\nimport { Check, Copy, X } from 'lucide-react'\nimport { AnimatePresence, MotionConfig, motion } from 'motion/react'\nimport { useEffect, useState } from 'react'\nimport { cn } from '@/lib/utils'\n\ntype State = 'idle' | 'copied' | 'failed'\n\nexport type CopyButtonProps = {\n  /** The text that lands on the clipboard. */\n  value: string\n  labels?: { copy?: string; copied?: string; failed?: string }\n  className?: string\n}\n\n/** Copies a value, and the label and icon roll over to say it worked (or that the browser said no). */\nexport function CopyButton({ value, labels = {}, className }: CopyButtonProps) {\n  const { copy = 'Copy', copied = 'Copied', failed = 'Not copied' } = labels\n  const [state, setState] = useState<State>('idle')\n\n  useEffect(() => {\n    if (state === 'idle') return\n    const t = setTimeout(() => setState('idle'), 1800)\n    return () => clearTimeout(t)\n  }, [state])\n\n  const run = async () => {\n    try {\n      await navigator.clipboard.writeText(value)\n      setState('copied')\n    } catch {\n      setState('failed')\n    }\n  }\n\n  const text = { idle: copy, copied, failed }[state]\n  const Icon = { idle: Copy, copied: Check, failed: X }[state]\n\n  return (\n    <MotionConfig reducedMotion=\"user\">\n      <motion.button\n        type=\"button\"\n        onClick={run}\n        whileTap={{ scale: 0.97 }}\n        className={cn(\n          'inline-flex min-h-11 items-center gap-2 rounded-full px-4 text-sm font-medium transition-colors duration-300',\n          state === 'copied' ? 'bg-emerald-600 text-white' : state === 'failed' ? 'bg-destructive text-white' : 'bg-primary text-primary-foreground',\n          className,\n        )}\n      >\n        <AnimatePresence mode=\"popLayout\" initial={false}>\n          <motion.span\n            key={state}\n            initial={{ opacity: 0, scale: 0.3, filter: 'blur(4px)' }}\n            animate={{ opacity: 1, scale: 1, filter: 'blur(0px)' }}\n            exit={{ opacity: 0, scale: 0.3, filter: 'blur(4px)' }}\n            transition={{ type: 'spring', duration: 0.3, bounce: 0 }}\n          >\n            <Icon className=\"size-4\" aria-hidden />\n          </motion.span>\n        </AnimatePresence>\n        <span className=\"relative inline-flex\" aria-live=\"polite\">\n          <AnimatePresence mode=\"popLayout\" initial={false}>\n            <motion.span key={text} className=\"inline-flex\">\n              {text.split('').map((ch, i) => (\n                <motion.span\n                  key={i}\n                  initial={{ opacity: 0, y: 6, scale: 0.7 }}\n                  animate={{ opacity: 1, y: 0, scale: 1, transition: { type: 'spring', stiffness: 260, damping: 32, delay: i * 0.025 } }}\n                  exit={{ opacity: 0, y: -6, scale: 0.7 }}\n                  className=\"inline-block whitespace-pre\"\n                >\n                  {ch}\n                </motion.span>\n              ))}\n            </motion.span>\n          </AnimatePresence>\n        </span>\n      </motion.button>\n    </MotionConfig>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "meta": {
    "credit": "Adapted from Watermelon UI (MIT)."
  },
  "categories": [
    "controls"
  ],
  "type": "registry:component"
}