{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"copy-button","type":"registry:ui","title":"Copy Button","description":"Copies a value in one tag: an icon button with a tooltip, or a button with a label, that turns into a check and says \"Copied\".","dependencies":["lucide-react"],"registryDependencies":["https://dashral.com/r/utils.json","https://dashral.com/r/lib-locale.json","https://dashral.com/r/button.json","https://dashral.com/r/tooltip.json"],"files":[{"path":"registry/dashral/copy-button.tsx","type":"registry:file","target":"components/dashral/copy-button.tsx","content":"\"use client\"\n\nimport * as React from \"react\"\nimport { Check, Copy, X } from \"lucide-react\"\n\nimport { useLocale } from \"./lib/locale\"\nimport { cn } from \"./lib/utils\"\nimport { Button, type ButtonColor, type ButtonVariant } from \"./button\"\nimport { Tooltip } from \"./tooltip\"\n\ntype CopyState = \"idle\" | \"copied\" | \"failed\"\n\n/** Writes text to the clipboard; where the Clipboard API is missing or refused (plain http, an iframe without permission), a hidden textarea and execCommand. */\nasync function writeClipboard(text: string) {\n  try {\n    await navigator.clipboard.writeText(text)\n    return true\n  } catch {\n    const area = document.createElement(\"textarea\")\n    area.value = text\n    area.setAttribute(\"readonly\", \"\")\n    area.style.cssText = \"position:fixed;top:0;left:0;opacity:0;pointer-events:none\"\n    document.body.append(area)\n    area.select()\n    const done = document.execCommand(\"copy\")\n    area.remove()\n    return done\n  }\n}\n\ntype CopyButtonProps = Omit<React.ComponentProps<\"button\">, \"color\" | \"value\" | \"children\" | \"onCopy\"> & {\n  /** The text it copies. */\n  value: string\n  /** A word beside the icon (\"Copy link\"); it reads \"Copied\" for a moment after. Without it, an icon button with a tooltip. */\n  label?: string\n  /** How much it stands out, as on Button. Ghost by default. */\n  variant?: ButtonVariant\n  color?: ButtonColor\n  /** sm or default with a label; icon-sm (default) or icon without. */\n  size?: \"sm\" | \"default\" | \"icon-sm\" | \"icon\"\n  /** The tooltip on an icon button, before copying. Defaults to \"Copy\". */\n  tooltip?: string\n  /** Milliseconds the check (or the failure) stays before it's ready again. */\n  timeout?: number\n  /** Runs after a copy, e.g. to show a toast. */\n  onCopy?: (value: string) => void\n}\n\n/**\n * Copies `value` in one tag: `<CopyButton value={apiKey} />`. The copy icon blurs into a check, the tooltip (or the\n * label) says \"Copied\", and screen readers hear it; if the browser refuses, an × and \"Couldn't copy\". Variant, color\n * and size work as on Button, so it fits a code block, an input's suffix or a row of actions.\n */\nfunction CopyButton({\n  value,\n  label,\n  variant = \"ghost\",\n  color,\n  size,\n  tooltip,\n  timeout = 2000,\n  onCopy,\n  onClick,\n  className,\n  ...props\n}: CopyButtonProps) {\n  const { t } = useLocale()\n  const [state, setState] = React.useState<CopyState>(\"idle\")\n  const [hover, setHover] = React.useState(false)\n  const timer = React.useRef<ReturnType<typeof setTimeout>>(undefined)\n  React.useEffect(() => () => clearTimeout(timer.current), [])\n\n  async function copy(event: React.MouseEvent<HTMLButtonElement>) {\n    onClick?.(event)\n    if (event.defaultPrevented) return\n    const done = await writeClipboard(value)\n    setState(done ? \"copied\" : \"failed\")\n    if (done) onCopy?.(value)\n    clearTimeout(timer.current)\n    timer.current = setTimeout(() => setState(\"idle\"), timeout)\n  }\n\n  const said = state === \"copied\" ? t(\"copied\") : state === \"failed\" ? t(\"copyFailed\") : (label ?? tooltip ?? t(\"copy\"))\n  // The three icons share one spot; the ones not in use are blurred, shrunk and transparent.\n  const icon = (shown: boolean) =>\n    cn(\n      \"absolute inset-0 transition-[opacity,filter,scale] duration-(--duration-standard) ease-spring motion-reduce:transition-opacity\",\n      shown ? \"scale-100 opacity-100 blur-none\" : \"scale-50 opacity-0 blur-[2px]\"\n    )\n\n  const button = (\n    <Button\n      data-slot=\"copy-button\"\n      type=\"button\"\n      variant={variant}\n      color={color}\n      size={size ?? (label ? \"sm\" : \"icon-sm\")}\n      onClick={copy}\n      aria-label={label ? undefined : said}\n      className={cn(variant === \"ghost\" && !color && \"text-muted-foreground hover:text-foreground\", className)}\n      {...props}\n    >\n      <span aria-hidden className=\"relative size-4\">\n        <Copy className={icon(state === \"idle\")} />\n        <Check className={cn(icon(state === \"copied\"), \"text-success-text\")} />\n        <X className={cn(icon(state === \"failed\"), \"text-destructive-text\")} />\n      </span>\n      {label && <span>{said}</span>}\n    </Button>\n  )\n\n  return (\n    <>\n      {label ? (\n        button\n      ) : (\n        // Open while hovered or focused, and through the \"Copied\" that follows a click (which would otherwise close it).\n        <Tooltip content={said} open={hover || state !== \"idle\"} onOpenChange={setHover}>\n          {button}\n        </Tooltip>\n      )}\n      {/* Says what happened: the label or tooltip changes too, but a screen reader only hears a live region. Outside\n          the button, so it isn't part of the button's name. */}\n      <span role=\"status\" className=\"sr-only\">\n        {state === \"idle\" ? \"\" : said}\n      </span>\n    </>\n  )\n}\n\nexport { CopyButton, type CopyButtonProps }\n"}],"cssVars":{"theme":{"color-destructive-foreground":"var(--destructive-foreground)","color-destructive-text":"var(--destructive-text)","color-success":"var(--success)","color-success-foreground":"var(--success-foreground)","color-success-text":"var(--success-text)","ease-standard":"var(--ease-standard)","ease-enter":"var(--ease-enter)","ease-exit":"var(--ease-exit)","ease-spring":"var(--ease-spring)"},"light":{"destructive-foreground":"oklch(0.985 0 0)","destructive-text":"#bd2800","success":"oklch(0.508 0.118 165.612)","success-foreground":"oklch(0.985 0 0)","success-text":"oklch(0.432 0.095 166.913)","duration-fast":"160ms","duration-standard":"240ms","duration-considered":"480ms","ease-standard":"cubic-bezier(0.2, 0, 0, 1)","ease-enter":"cubic-bezier(0, 0, 0, 1)","ease-exit":"cubic-bezier(0.3, 0, 1, 1)","ease-spring":"linear(0, 0.071 4%, 0.234 8%, 0.433 12%, 0.626 17%, 0.791 21%, 0.917 25%, 1.004 29%, 1.055 33%, 1.079 38%, 1.083 42%, 1.075 46%, 1.06 50%, 1.043 54%, 1.027 58%, 1.015 62%, 1.005 67%, 0.998 71%, 0.995 75%, 0.993 79%, 0.993 83%, 0.994 88%, 0.995 92%, 0.997 96%, 1)"},"dark":{"destructive-foreground":"oklch(0.2 0 0)","destructive-text":"#ff8a5c","success":"oklch(0.696 0.17 162.48)","success-foreground":"oklch(0.2 0.04 160)","success-text":"oklch(0.765 0.177 163.223)"}}}