{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "copy-button",
  "title": "Copy Button",
  "description": "A button that copies text and shows a tick for a moment.",
  "dependencies": [
    "@hugeicons/core-free-icons",
    "@hugeicons/react",
    "@stylexjs/stylex"
  ],
  "registryDependencies": [
    "https://constella.aayush.cv/r/base.json",
    "https://constella.aayush.cv/r/button.json",
    "https://constella.aayush.cv/r/swap-icon.json"
  ],
  "files": [
    {
      "path": "src/components/primitives/copy-button.tsx",
      "content": "\"use client\";\n\nimport { Copy01Icon, Tick02Icon } from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport * as stylex from \"@stylexjs/stylex\";\nimport { useEffect, useRef, useState } from \"react\";\n\nimport { Button } from \"@/components/primitives/button\";\nimport type { ButtonProps } from \"@/components/primitives/button\";\nimport { SwapIcon, SwapIconItem } from \"@/components/primitives/swap-icon\";\nimport { controlIconSize, visuallyHidden } from \"@/lib/constella/styles\";\nimport { durations, strokes } from \"@/lib/constella/tokens.stylex\";\n\ntype CopyButtonProps = Omit<ButtonProps, \"onClick\" | \"value\"> & {\n  /** The text, or a function read at the press for text only the page holds, like highlighted code. */\n  value: string | (() => string);\n  /** The same content as HTML, for a target that takes it: a spreadsheet pastes a table as cells. */\n  html?: string | (() => string);\n};\n\nfunction read(content: string | (() => string)) {\n  // oxlint-disable-next-line anti-slop/no-runtime-typeof -- content is a documented union of text or its getter\n  return typeof content === \"string\" ? content : content();\n}\n\n// The Web clock takes bare milliseconds; the token carries `ms`.\nconst confirmFor = Number(durations.confirm.slice(0, -\"ms\".length));\n\n/** Copies `value`, and shows a tick for a moment after. */\nfunction CopyButton({\n  \"aria-label\": label,\n  children,\n  html,\n  size,\n  value,\n  variant = \"ghost\",\n  ...props\n}: CopyButtonProps) {\n  const [copies, setCopies] = useState(0);\n  const copied = copies > 0;\n  const timeout = useRef<ReturnType<typeof setTimeout>>(null);\n\n  useEffect(\n    () => () => {\n      clearTimeout(timeout.current ?? undefined);\n    },\n    []\n  );\n\n  async function copy() {\n    const text = read(value);\n    await (html === undefined\n      ? navigator.clipboard.writeText(text)\n      : navigator.clipboard.write([\n          new ClipboardItem({\n            \"text/html\": new Blob([read(html)], { type: \"text/html\" }),\n            \"text/plain\": new Blob([text], { type: \"text/plain\" }),\n          }),\n        ]));\n    setCopies((count) => count + 1);\n    clearTimeout(timeout.current ?? undefined);\n    timeout.current = setTimeout(() => {\n      setCopies(0);\n    }, confirmFor);\n  }\n\n  const labelled = children !== undefined;\n  const buttonSize = size ?? (labelled ? \"sm\" : \"icon-sm\");\n  const iconSize = controlIconSize[buttonSize];\n\n  return (\n    <>\n      <Button\n        aria-label={label ?? (labelled ? undefined : \"Copy\")}\n        data-slot=\"copy-button\"\n        onClick={() => {\n          void copy();\n        }}\n        size={buttonSize}\n        variant={variant}\n        {...props}\n      >\n        <SwapIcon\n          aria-hidden\n          data-icon={labelled ? \"inline-start\" : undefined}\n          value={copied ? \"copied\" : \"copy\"}\n        >\n          <SwapIconItem value=\"copied\">\n            <HugeiconsIcon\n              icon={Tick02Icon}\n              size={iconSize}\n              strokeWidth={Number(strokes.icon)}\n            />\n          </SwapIconItem>\n          <SwapIconItem value=\"copy\">\n            <HugeiconsIcon\n              icon={Copy01Icon}\n              size={iconSize}\n              strokeWidth={Number(strokes.icon)}\n            />\n          </SwapIconItem>\n        </SwapIcon>\n        {children}\n      </Button>\n      {/* Outside the button, or its text would join the button's name. */}\n      <output aria-live=\"polite\" {...stylex.props(visuallyHidden.base)}>\n        {/* A fresh node per press, so a repeat press is announced again. */}\n        {copied && <span key={copies}>Copied</span>}\n      </output>\n    </>\n  );\n}\n\nexport { CopyButton };\nexport type { CopyButtonProps };\n",
      "type": "registry:component"
    }
  ],
  "docs": "Usage: https://constella.aayush.cv/docs/components/copy-button/index.md",
  "type": "registry:ui"
}