{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "copy-button",
  "type": "registry:component",
  "title": "Copy Button",
  "description": "Click-to-copy utility with confirmation feedback and a useCopyToClipboard hook.",
  "dependencies": [
    "@vllnt/ui@^0.2.1",
    "lucide-react"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/copy-button/copy-button.tsx",
      "content": "\"use client\";\n\nimport {\n  type ComponentPropsWithoutRef,\n  forwardRef,\n  type MouseEvent as ReactMouseEvent,\n  type ReactElement,\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useRef,\n  useState,\n} from \"react\";\n\nimport { Check, Copy } from \"lucide-react\";\n\nimport { cn } from \"@vllnt/ui\";\nimport { Button } from \"@vllnt/ui\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@vllnt/ui\";\n\nconst FALLBACK_TIMEOUT_MS = 2000;\n\n/**\n * Options for {@link useCopyToClipboard}.\n *\n * @public\n */\nexport type UseCopyToClipboardOptions = {\n  /** Milliseconds the `copied` flag stays true after a successful copy. */\n  timeout?: number;\n};\n\n/**\n * Return shape for {@link useCopyToClipboard}.\n *\n * @public\n */\nexport type UseCopyToClipboardResult = {\n  /** True for `timeout` ms after the most recent successful copy. */\n  copied: boolean;\n  /** Writes `value` to the clipboard. Resolves to `true` on success. */\n  copy: (value: string) => Promise<boolean>;\n  /** Clears the `copied` flag and pending timer. */\n  reset: () => void;\n};\n\n/**\n * React hook that copies arbitrary strings to the clipboard with a transient\n * `copied` flag suitable for visual feedback.\n *\n * @example\n * ```tsx\n * const { copied, copy } = useCopyToClipboard()\n * <button onClick={() => copy(apiKey)}>{copied ? \"Copied!\" : \"Copy\"}</button>\n * ```\n *\n * @public\n */\nexport function useCopyToClipboard(\n  options: UseCopyToClipboardOptions = {},\n): UseCopyToClipboardResult {\n  const { timeout = FALLBACK_TIMEOUT_MS } = options;\n  const [copied, setCopied] = useState(false);\n  const timerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n\n  useEffect(() => {\n    return () => {\n      if (timerRef.current !== undefined) {\n        clearTimeout(timerRef.current);\n      }\n    };\n  }, []);\n\n  const reset = useCallback(() => {\n    if (timerRef.current !== undefined) clearTimeout(timerRef.current);\n    setCopied(false);\n  }, []);\n\n  const copy = useCallback(\n    async (value: string): Promise<boolean> => {\n      try {\n        if (\n          typeof navigator === \"undefined\" ||\n          typeof navigator.clipboard?.writeText !== \"function\"\n        ) {\n          return false;\n        }\n        await navigator.clipboard.writeText(value);\n        if (timerRef.current !== undefined) clearTimeout(timerRef.current);\n        setCopied(true);\n        timerRef.current = setTimeout(() => {\n          setCopied(false);\n        }, timeout);\n        return true;\n      } catch {\n        return false;\n      }\n    },\n    [timeout],\n  );\n\n  return { copied, copy, reset };\n}\n\n/**\n * Visual variant for {@link CopyButton}.\n *\n * - `icon`   — compact icon-style button (default), suitable for toolbars.\n * - `inline` — small button sized to sit next to short inline text.\n * - `button` — full button with text label, suitable for primary CTAs.\n *\n * @public\n */\nexport type CopyButtonVariant = \"button\" | \"icon\" | \"inline\";\n\ntype ButtonElementProps = ComponentPropsWithoutRef<\"button\">;\n\n/**\n * Props for {@link CopyButton}.\n *\n * @public\n */\nexport type CopyButtonProps = {\n  /** Tooltip + announcement text after a successful copy. Defaults to `\"Copied!\"`. */\n  copiedLabel?: string;\n  /** Class name forwarded to the rendered icon. */\n  iconClassName?: string;\n  /** Tooltip + accessible label before copy. Defaults to `\"Copy\"`. */\n  label?: string;\n  /** Set to `false` to suppress the tooltip. */\n  showTooltip?: boolean;\n  /** Milliseconds the success state persists. Defaults to 2000. */\n  timeout?: number;\n  /** String to write to the clipboard when clicked. */\n  value: string;\n  /** Visual variant. */\n  variant?: CopyButtonVariant;\n} & Omit<ButtonElementProps, \"value\">;\n\nfunction CopyIcon({\n  className,\n  copied,\n  size,\n}: {\n  className?: string;\n  copied: boolean;\n  size: \"sm\" | \"xs\";\n}): ReactNode {\n  const Icon = copied ? Check : Copy;\n  const sizeClass = size === \"xs\" ? \"size-3\" : \"size-4\";\n  return <Icon aria-hidden=\"true\" className={cn(sizeClass, className)} />;\n}\n\ntype TriggerProps = Omit<\n  CopyButtonProps,\n  \"copiedLabel\" | \"showTooltip\" | \"timeout\"\n> & {\n  accessibleLabel: string;\n  copied: boolean;\n  copiedText: string;\n  onClickHandler: (event: ReactMouseEvent<HTMLButtonElement>) => void;\n};\n\nconst ButtonTrigger = forwardRef<HTMLButtonElement, TriggerProps>(\n  (\n    {\n      accessibleLabel,\n      className,\n      copied,\n      copiedText,\n      iconClassName,\n      label = \"Copy\",\n      onClick: _onClick,\n      onClickHandler,\n      value: _value,\n      variant = \"icon\",\n      ...rest\n    },\n    ref,\n  ) => {\n    if (variant === \"button\") {\n      return (\n        <Button\n          aria-label={accessibleLabel}\n          className={cn(className)}\n          onClick={onClickHandler}\n          ref={ref}\n          size=\"sm\"\n          type=\"button\"\n          variant=\"outline\"\n          {...rest}\n        >\n          <CopyIcon className={iconClassName} copied={copied} size=\"sm\" />\n          <span>{copied ? copiedText : label}</span>\n        </Button>\n      );\n    }\n\n    if (variant === \"inline\") {\n      return (\n        <Button\n          aria-label={accessibleLabel}\n          className={cn(\n            \"size-6 align-middle text-muted-foreground hover:text-foreground\",\n            className,\n          )}\n          onClick={onClickHandler}\n          ref={ref}\n          size=\"icon\"\n          type=\"button\"\n          variant=\"ghost\"\n          {...rest}\n        >\n          <CopyIcon className={iconClassName} copied={copied} size=\"xs\" />\n        </Button>\n      );\n    }\n\n    return (\n      <Button\n        aria-label={accessibleLabel}\n        className={cn(\"size-8\", className)}\n        onClick={onClickHandler}\n        ref={ref}\n        size=\"icon\"\n        type=\"button\"\n        variant=\"ghost\"\n        {...rest}\n      >\n        <CopyIcon className={iconClassName} copied={copied} size=\"sm\" />\n      </Button>\n    );\n  },\n);\nButtonTrigger.displayName = \"CopyButton.Trigger\";\n\n/**\n * Click-to-copy button with confirmation feedback.\n *\n * Composes {@link Button} and {@link Tooltip}. Copies `value` to the clipboard\n * via the async Clipboard API and announces success to assistive tech via a\n * visually hidden status region.\n *\n * @example\n * ```tsx\n * <CopyButton value={apiKey} />\n * <CopyButton value={url} variant=\"button\" label=\"Copy link\" />\n * <span>ID: usr_42 <CopyButton value=\"usr_42\" variant=\"inline\" /></span>\n * ```\n *\n * @public\n */\nexport const CopyButton = forwardRef<HTMLButtonElement, CopyButtonProps>(\n  (\n    {\n      \"aria-label\": ariaLabelOverride,\n      copiedLabel = \"Copied!\",\n      label = \"Copy\",\n      onClick,\n      showTooltip = true,\n      timeout = FALLBACK_TIMEOUT_MS,\n      value,\n      ...rest\n    },\n    ref,\n  ) => {\n    const { copied, copy } = useCopyToClipboard({ timeout });\n\n    const handleClick = useCallback(\n      (event: ReactMouseEvent<HTMLButtonElement>) => {\n        onClick?.(event);\n        if (event.defaultPrevented) return;\n        void copy(value);\n      },\n      [copy, onClick, value],\n    );\n\n    const accessibleLabel = ariaLabelOverride ?? (copied ? copiedLabel : label);\n    const tooltipText = copied ? copiedLabel : label;\n\n    const trigger: ReactElement = (\n      <ButtonTrigger\n        {...rest}\n        accessibleLabel={accessibleLabel}\n        copied={copied}\n        copiedText={copiedLabel}\n        label={label}\n        onClickHandler={handleClick}\n        ref={ref}\n        value={value}\n      />\n    );\n\n    const liveRegion = (\n      <span aria-live=\"polite\" className=\"sr-only\" role=\"status\">\n        {copied ? copiedLabel : \"\"}\n      </span>\n    );\n\n    if (!showTooltip) {\n      return (\n        <>\n          {trigger}\n          {liveRegion}\n        </>\n      );\n    }\n\n    return (\n      <TooltipProvider delayDuration={200}>\n        <Tooltip>\n          <TooltipTrigger asChild>{trigger}</TooltipTrigger>\n          <TooltipContent>{tooltipText}</TooltipContent>\n        </Tooltip>\n        {liveRegion}\n      </TooltipProvider>\n    );\n  },\n);\nCopyButton.displayName = \"CopyButton\";\n",
      "type": "registry:component"
    }
  ],
  "version": "0.2.1",
  "stability": "stable"
}
