{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "copy-button",
  "type": "registry:ui",
  "title": "Copy button",
  "description": "Copy a value with immediate confirmation.",
  "categories": [
    "actions"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "https://irisui.dev/r/foundation.json",
    "https://irisui.dev/r/utils.json",
    "https://irisui.dev/r/motion-tokens.json",
    "https://irisui.dev/r/use-copy-feedback.json"
  ],
  "files": [
    {
      "path": "src/registry/components/copy-button/copy-button.tsx",
      "content": "\"use client\";\n\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { CircleAlert, Copy } from \"lucide-react\";\nimport { cn } from \"../lib/utils\";\nimport { motionTokens } from \"../lib/motion-tokens\";\nimport { useCopyFeedback } from \"../lib/use-copy-feedback\";\n\nexport interface CopyButtonProps {\n  value: string;\n  label?: string;\n  /** Text shown once the value is on the clipboard. */\n  copiedLabel?: string;\n  className?: string;\n  iconOnly?: boolean;\n  variant?: \"outline\" | \"plain\";\n  disabled?: boolean;\n  onCopied?: () => void;\n}\n\n/** Copy feedback is quick and quiet: the old icon and word fade out fast while the new ones fade in, with no blur and no\n *  per-letter motion. The label cell reserves its widest state, so the button never changes width. */\nconst ease = [...motionTokens.ease.standard] as [number, number, number, number];\nconst show = { duration: 0.16, ease } as const;\nconst hide = { duration: 0.1, ease } as const;\nconst instant = { duration: 0 } as const;\n\n/** The success tick draws itself in one short stroke. */\nfunction DrawnCheck({ reduced }: { reduced: boolean }) {\n  return (\n    <svg\n      width={16}\n      height={16}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={1.75}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    >\n      <motion.path\n        d=\"M4 12l5 5L20 6\"\n        initial={reduced ? false : { pathLength: 0 }}\n        animate={{ pathLength: 1 }}\n        transition={{ duration: 0.22, ease, delay: 0.04 }}\n      />\n    </svg>\n  );\n}\n\nexport function CopyButton({\n  value,\n  label = \"Copy\",\n  copiedLabel = \"Copied\",\n  className,\n  iconOnly = false,\n  variant = \"outline\",\n  disabled,\n  onCopied,\n}: CopyButtonProps) {\n  const { state, copy } = useCopyFeedback();\n  const reduced = useReducedMotion() ?? false;\n  const text = state === \"copied\" ? copiedLabel : state === \"error\" ? \"Failed\" : label;\n\n  async function handleCopy() {\n    if (await copy(value)) onCopied?.();\n  }\n\n  // Width never changes: the label cell reserves its widest state while icon and letters crossfade inside it. Press releases on the spring curve.\n  return (\n    <>\n      <button\n        type=\"button\"\n        className={cn(\n          \"inline-flex min-h-control-sm w-max max-w-full cursor-pointer items-center justify-center gap-2 rounded-button border border-line bg-surface px-3 text-sm/[inherit] font-medium text-ink [-webkit-tap-highlight-color:transparent] [transition:background-color_var(--duration-fast)_var(--ease-standard),border-color_var(--duration-fast)_var(--ease-standard),color_var(--duration-fast)_var(--ease-standard),transform_var(--duration-spring)_var(--ease-spring)] disabled:cursor-not-allowed disabled:opacity-50\",\n          \"enabled:active:[transition-duration:var(--duration-fast),var(--duration-fast),var(--duration-fast),var(--duration-instant)] enabled:active:[transition-timing-function:var(--ease-standard)] motion-reduce:transition-none motion-reduce:enabled:active:transform-none\",\n          iconOnly\n            ? \"w-control-sm px-0 enabled:active:[transform:scale(.96)]\"\n            : \"enabled:active:[transform:scale(.97)]\",\n          variant === \"plain\"\n            ? \"border-transparent bg-transparent text-ink-2 enabled:hover:border-transparent enabled:hover:bg-hover enabled:hover:text-ink\"\n            : \"enabled:hover:border-line-strong enabled:hover:bg-hover\",\n          className,\n        )}\n        onClick={() => void handleCopy()}\n        aria-label={label}\n        data-copy-state={state}\n        disabled={disabled}\n      >\n        <span className=\"relative grid size-4 flex-none\" aria-hidden=\"true\">\n          <AnimatePresence initial={false}>\n            {/* Each icon carries its own color, so the outgoing glyph never tints on its way out. */}\n            <motion.span\n              key={state}\n              className=\"absolute inset-0 grid place-items-center data-[state=copied]:text-green data-[state=error]:text-red\"\n              data-state={state}\n              initial={reduced ? { opacity: 0 } : { opacity: 0, scale: 0.85 }}\n              animate={{ opacity: 1, scale: 1 }}\n              exit={{ opacity: 0, transition: reduced ? instant : hide }}\n              transition={reduced ? instant : show}\n            >\n              {state === \"copied\" ? (\n                <DrawnCheck reduced={reduced} />\n              ) : state === \"error\" ? (\n                <CircleAlert size={16} strokeWidth={1.75} />\n              ) : (\n                <Copy size={16} strokeWidth={1.75} />\n              )}\n            </motion.span>\n          </AnimatePresence>\n        </span>\n        {!iconOnly && (\n          <span className=\"grid min-w-0 text-left whitespace-nowrap\" aria-hidden=\"true\">\n            <span className=\"invisible col-start-1 row-start-1\">{label}</span>\n            <span className=\"invisible col-start-1 row-start-1\">{copiedLabel}</span>\n            <span className=\"invisible col-start-1 row-start-1\">Failed</span>\n            <AnimatePresence initial={false}>\n              <motion.span\n                key={text}\n                className=\"col-start-1 row-start-1 justify-self-start\"\n                initial={reduced ? { opacity: 0 } : { opacity: 0, y: 2 }}\n                animate={{ opacity: 1, y: 0 }}\n                exit={{ opacity: 0, transition: reduced ? instant : hide }}\n                transition={reduced ? instant : show}\n              >\n                {text}\n              </motion.span>\n            </AnimatePresence>\n          </span>\n        )}\n      </button>\n      <output className=\"sr-only\" aria-live=\"polite\">\n        {state === \"idle\" ? \"\" : state === \"error\" ? `${label}: Could not copy` : `${label}: ${copiedLabel}`}\n      </output>\n    </>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/iris/copy-button/copy-button.tsx"
    }
  ],
  "docs": "Docs and live preview: https://irisui.dev/components/copy-button",
  "meta": {
    "kind": "component",
    "group": "Actions",
    "export": "CopyButton",
    "tags": [
      "action",
      "utility"
    ],
    "docs": "https://irisui.dev/components/copy-button"
  }
}
