{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "eased-copy",
  "title": "Copy primitive",
  "description": "The copy → check morph: a clipboard write with a textarea fallback, a useCopy hook that holds the copied state for 1.6s, the copy icon that swaps to a check with a blur, and the CopyButton built on them. Shared by the code components, Wallet Card and Connect Wallet.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://eased.dev/r/eased-tokens.json",
    "https://eased.dev/r/eased-motion.json",
    "https://eased.dev/r/eased-text-swap.json"
  ],
  "files": [
    {
      "path": "src/registry/eased/_primitives/copy/copy.tsx",
      "content": "\"use client\";\n\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { useCallback, useEffect, useRef, useState, type ComponentPropsWithoutRef } from \"react\";\nimport { TextSwap } from \"../text-swap/text-swap\";\nimport { exitFast, fade, reducedFade, snappy, swap, swapIn, swapOut, swapShown, useSafeReducedMotion } from \"../../lib/motion\";\nimport { pressIcon } from \"../../lib/press\";\n\n/** How long the check stays before the copy icon returns, ms. */\nexport const COPIED_MS = 1600;\n\n/** Write text to the clipboard, falling back to a hidden textarea where the async API is blocked. */\nexport async function writeClipboard(text: string): Promise<boolean> {\n  try {\n    await navigator.clipboard.writeText(text);\n    return true;\n  } catch {\n    try {\n      const el = document.createElement(\"textarea\");\n      el.value = text;\n      el.setAttribute(\"readonly\", \"\");\n      el.style.cssText = \"position:fixed;opacity:0;pointer-events:none\";\n      document.body.append(el);\n      el.select();\n      const ok = document.execCommand(\"copy\");\n      el.remove();\n      return ok;\n    } catch {\n      return false;\n    }\n  }\n}\n\n/**\n * Copy state for any trigger: `copy()` writes the text and flips `copied` for `COPIED_MS`.\n * Copying again restarts the timer, so the check never flickers off between quick copies.\n */\nexport function useCopy(text: string | (() => string), { timeout = COPIED_MS, onCopy }: { timeout?: number; onCopy?: (text: string) => void } = {}) {\n  const [copied, setCopied] = useState(false);\n  const timer = useRef<number | undefined>(undefined);\n  useEffect(() => () => window.clearTimeout(timer.current), []);\n  const copy = useCallback(async () => {\n    const value = typeof text === \"function\" ? text() : text;\n    if (!(await writeClipboard(value))) return false;\n    onCopy?.(value);\n    setCopied(true);\n    window.clearTimeout(timer.current);\n    timer.current = window.setTimeout(() => setCopied(false), timeout);\n    return true;\n  }, [text, timeout, onCopy]);\n  return { copied, copy };\n}\n\n/**\n * The copy → check morph. The two sheets lift and blur away while the check draws itself in\n * on the snappy spring; going back, the check leaves faster than it came (exit ≤ 0.75 ×\n * enter). Reduced motion: a plain cross-fade.\n */\nexport function CopyIcon({ copied, className = \"\" }: { copied: boolean; className?: string }) {\n  const reduced = useSafeReducedMotion();\n  // Both icons share one grid cell, so the swap never changes the layout (popLayout cannot lift an SVG out of flow).\n  return (\n    <span aria-hidden className={`grid size-4 shrink-0 place-items-center *:col-start-1 *:row-start-1 ${className}`}>\n      <AnimatePresence initial={false}>\n        {copied ? (\n          <motion.svg\n            key=\"check\"\n            viewBox=\"0 0 16 16\"\n            fill=\"none\"\n            className=\"size-4 overflow-visible\"\n            initial={reduced ? { opacity: 0 } : { opacity: 0, scale: 0.8, filter: \"blur(2px)\" }}\n            animate={{ opacity: 1, scale: 1, filter: \"blur(0px)\" }}\n            exit={reduced ? { opacity: 0, transition: reducedFade } : { ...swapOut, transition: { ...swap, opacity: exitFast, filter: exitFast } }}\n            transition={reduced ? reducedFade : { ...snappy, opacity: fade, filter: fade }}\n          >\n            <motion.path\n              d=\"M3.25 8.5 6.5 11.75 12.75 4.75\"\n              stroke=\"currentColor\"\n              strokeWidth={1.5}\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              initial={reduced ? false : { pathLength: 0 }}\n              animate={{ pathLength: 1 }}\n              transition={{ ...snappy, delay: 0.04 }}\n            />\n          </motion.svg>\n        ) : (\n          <motion.svg\n            key=\"copy\"\n            viewBox=\"0 0 16 16\"\n            fill=\"none\"\n            className=\"size-4\"\n            initial={reduced ? { opacity: 0 } : swapIn}\n            animate={reduced ? { opacity: 1 } : swapShown}\n            exit={reduced ? { opacity: 0, transition: reducedFade } : { opacity: 0, scale: 0.85, filter: \"blur(2px)\", transition: { ...snappy, opacity: exitFast, filter: exitFast } }}\n            transition={reduced ? reducedFade : swap}\n          >\n            <rect x=\"5.25\" y=\"5.25\" width=\"8\" height=\"8\" rx=\"2\" stroke=\"currentColor\" strokeWidth=\"1.5\" />\n            <path d=\"M10.75 5.25V4.5a1.75 1.75 0 0 0-1.75-1.75H4.5A1.75 1.75 0 0 0 2.75 4.5V9a1.75 1.75 0 0 0 1.75 1.75h.75\" stroke=\"currentColor\" strokeWidth=\"1.5\" />\n          </motion.svg>\n        )}\n      </AnimatePresence>\n    </span>\n  );\n}\n\nexport type CopyButtonProps = Omit<ComponentPropsWithoutRef<\"button\">, \"onClick\" | \"children\" | \"onCopy\"> & {\n  /** The text to copy, or a function that returns it at click time. */\n  text: string | (() => string);\n  /** Accessible name. Default \"Copy code\". */\n  label?: string;\n  /**\n   * Slide a \"Copied\" label out to the left of the check (the button grows leftward on a\n   * spring and shrinks back). Default true; pass false where there is no room.\n   */\n  showLabel?: boolean;\n  /** Extend the target to 44×44px. Default true; pass false inside a row of other targets. */\n  hitArea?: boolean;\n  /** Radius class. Default `rounded-control-sm`; pass `radius-nested` inside a padded control. */\n  radius?: string;\n  onCopy?: (text: string) => void;\n};\n\n/**\n * A 32px ghost copy button with the copy → check morph. Announces \"Copied\" politely to\n * screen readers. Its right edge stays put when the label slides out, so park it at the\n * end of a row.\n */\nexport function CopyButton({ text, label = \"Copy code\", showLabel = true, hitArea = true, radius = \"rounded-control-sm\", onCopy, className = \"\", ...props }: CopyButtonProps) {\n  const { copied, copy } = useCopy(text, { onCopy });\n  return (\n    <>\n      <button\n        type=\"button\"\n        aria-label={label}\n        {...props}\n        onClick={() => void copy()}\n        className={`relative inline-flex h-8 min-w-8 shrink-0 cursor-pointer items-center justify-center ${radius} px-2 text-ui-sm font-medium whitespace-nowrap select-none ${\n          copied ? \"text-text\" : \"text-text-muted hover-fine:text-text\"\n        } hover-fine:bg-surface-hover active:bg-surface-active ${pressIcon} ${hitArea ? \"hit-area\" : \"\"} ${className}`}\n      >\n        {showLabel && (\n          <TextSwap swapKey={copied ? \"copied\" : \"idle\"} aria-hidden>\n            {copied ? <span className=\"pr-1.5\">Copied</span> : null}\n          </TextSwap>\n        )}\n        <CopyIcon copied={copied} />\n      </button>\n      <span role=\"status\" className=\"sr-only\">\n        {copied ? \"Copied\" : \"\"}\n      </span>\n    </>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@components/eased/_primitives/copy/copy.tsx"
    }
  ],
  "type": "registry:ui"
}