{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ui-copy-button",
  "title": "Copy button",
  "description": "Copy-to-clipboard button whose icon turns into a drawn check as the label rolls to \"Copied\"; it says so when the browser refuses.",
  "registryDependencies": [
    "https://elements.lumesec.ai/r/lumesec-theme.json",
    "https://elements.lumesec.ai/r/lumesec-motion.json",
    "https://elements.lumesec.ai/r/lumesec-roll-text.json",
    "https://elements.lumesec.ai/r/lumesec-sparks.json",
    "utils"
  ],
  "files": [
    {
      "path": "registry/components/lumesec/ui-copy-button.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { prefersReducedMotion } from \"@/lib/lumesec/motion\";\nimport { RollText } from \"@/lib/lumesec/roll-text\";\nimport { BurstLayer, useBurst } from \"@/lib/lumesec/sparks\";\nimport { cn } from \"@/lib/utils\";\n\n/** Feedback state: `done` after a successful copy, `fail` when the browser refused, for 1.8 s. */\nexport type CopyButtonState = \"idle\" | \"done\" | \"fail\";\n\nexport interface UiCopyButtonProps extends Omit<React.ComponentProps<\"button\">, \"onCopy\" | \"children\"> {\n  /** Text written to the clipboard. Read on each click. */\n  text?: string;\n  /** Idle label. */\n  label?: string;\n  /** Label shown for 1.8 s after a successful copy. */\n  doneLabel?: string;\n  /** Called after each copy attempt; `ok` is false when the clipboard write was rejected. */\n  onCopy?: (ok: boolean, text: string) => void;\n}\n\nconst FAIL_LABEL = \"Couldn’t copy\";\nconst RESET_MS = 1800;\n\n/**\n * Copy-to-clipboard button whose icon turns into a drawn check as the label rolls to \"Copied\"; it says\n * so when the browser refuses. The label is a polite live region.\n *\n * @example\n * <UiCopyButton text=\"npx shadcn@latest add @lumesec/ui-copy-button\" />\n */\nexport function UiCopyButton({\n  text = \"\",\n  label = \"Copy\",\n  doneLabel = \"Copied\",\n  onCopy,\n  className,\n  onClick,\n  ...props\n}: UiCopyButtonProps) {\n  const [state, setState] = React.useState<CopyButtonState>(\"idle\");\n  const iconRef = React.useRef<HTMLSpanElement>(null);\n  const checkRef = React.useRef<SVGSVGElement>(null);\n  const { ref: sparksRef, fire: fireSparks } = useBurst();\n  const resetTimer = React.useRef(0);\n  // the clipboard write is async: a button removed meanwhile only reports the result\n  const mounted = React.useRef(false);\n\n  React.useEffect(() => {\n    mounted.current = true;\n    return () => {\n      mounted.current = false;\n      window.clearTimeout(resetTimer.current);\n    };\n  }, []);\n\n  const copy = async () => {\n    let ok = true;\n    try {\n      await navigator.clipboard.writeText(text);\n    } catch {\n      ok = false;\n    }\n    onCopy?.(ok, text);\n    if (!mounted.current) return;\n    setState(ok ? \"done\" : \"fail\");\n    if (ok && !prefersReducedMotion()) {\n      checkRef.current?.animate(\n        [{ transform: \"scale(.6)\" }, { transform: \"scale(1.3)\", offset: 0.5 }, { transform: \"scale(1)\" }],\n        {\n          duration: 440,\n          easing: \"cubic-bezier(.2,.8,.2,1)\",\n        },\n      );\n      if (iconRef.current) {\n        fireSparks(iconRef.current, {\n          count: 16,\n          colors: [\"--lumesec-success\", \"--lumesec\", \"--lumesec-glint\"],\n          speed: [40, 150],\n          life: [0.3, 0.65],\n        });\n      }\n    }\n    window.clearTimeout(resetTimer.current);\n    resetTimer.current = window.setTimeout(() => setState(\"idle\"), RESET_MS);\n  };\n\n  const shown = state === \"done\" ? doneLabel : state === \"fail\" ? FAIL_LABEL : label;\n\n  return (\n    <button\n      type=\"button\"\n      data-slot=\"ui-copy-button\"\n      data-state={state}\n      className={cn(\n        \"group relative inline-flex h-[34px] cursor-pointer items-center gap-2 rounded-[9px] border border-border bg-card pr-3.5 pl-[11px] font-[inherit] text-[13.5px] leading-normal font-medium text-foreground outline-none select-none\",\n        \"[transition:transform_.15s_cubic-bezier(.2,.8,.2,1),border-color_.3s,background-color_.2s,color_.3s] hover:bg-muted active:[transform:scale(.95)] disabled:pointer-events-none disabled:opacity-50\",\n        \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-lumesec focus-visible:outline-solid\",\n        \"data-[state=done]:border-[color-mix(in_srgb,var(--lumesec-success)_50%,var(--border))]\",\n        \"data-[state=fail]:border-[color-mix(in_srgb,var(--destructive)_50%,var(--border))] data-[state=fail]:text-destructive\",\n        className,\n      )}\n      onClick={(event) => {\n        onClick?.(event);\n        if (!event.defaultPrevented) void copy();\n      }}\n      {...props}\n    >\n      <span ref={iconRef} className=\"relative grid size-4\">\n        <svg\n          viewBox=\"0 0 16 16\"\n          aria-hidden=\"true\"\n          className=\"col-start-1 row-start-1 size-4 fill-none stroke-current [stroke-width:1.6] [stroke-linecap:round] [stroke-linejoin:round] [transition:opacity_.18s_ease,transform_.35s_cubic-bezier(.2,.8,.2,1)] group-data-[state=done]:[transform:scale(.4)_rotate(-25deg)] group-data-[state=done]:opacity-0\"\n        >\n          <rect x=\"5.2\" y=\"5.2\" width=\"8.3\" height=\"8.3\" rx=\"1.8\" />\n          <path d=\"M10.8 5.2V4.1c0-1-.8-1.6-1.6-1.6H4.1c-.9 0-1.6.7-1.6 1.6v5.1c0 .9.7 1.6 1.6 1.6h1.1\" />\n        </svg>\n        <svg\n          ref={checkRef}\n          viewBox=\"0 0 16 16\"\n          aria-hidden=\"true\"\n          className=\"col-start-1 row-start-1 size-4 fill-none stroke-current [stroke-width:2] text-lumesec-success opacity-0 [stroke-linecap:round] [stroke-linejoin:round] [transition:opacity_.15s] group-data-[state=done]:opacity-100\"\n        >\n          <path\n            d=\"M3.3 8.6l3 3 6.4-7\"\n            className=\"[stroke-dasharray:14] [stroke-dashoffset:14] [transition:stroke-dashoffset_.35s_cubic-bezier(.3,.7,.3,1)_.1s] group-data-[state=done]:[stroke-dashoffset:0]\"\n          />\n        </svg>\n      </span>\n      <RollText\n        text={shown}\n        direction={state === \"idle\" ? -1 : 1}\n        reserve={[label, doneLabel, FAIL_LABEL]}\n        aria-live=\"polite\"\n      />\n      <BurstLayer ref={sparksRef} />\n    </button>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/lumesec/ui-copy-button.tsx"
    }
  ],
  "meta": {
    "tag": "ui-copy-button",
    "collection": "interaction",
    "react": "UiCopyButton",
    "kind": "react"
  },
  "docs": "Copy button docs: https://elements.lumesec.ai/components/interaction/copy-button",
  "categories": [
    "interaction",
    "controls"
  ],
  "type": "registry:component"
}