{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "clipboard",
  "title": "Clipboard",
  "type": "registry:ui",
  "description": "Copy-to-clipboard button with success/error feedback. Swaps the icon to a check on success, shows a tooltip with configurable feedback text, supports a visible label, disabled state, a custom timeout for feedback reset, and copy/success/error events. Includes a legacy execCommand fallback for non-secure contexts. Composes the tooltip primitive.",
  "categories": [
    "utility",
    "control"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/clipboard/clipboard.ts",
      "content": "import { LitElement, css, html, isServer, nothing } from 'lit'\nimport { styleMap } from 'lit/directives/style-map.js'\nimport { Check, Copy } from 'lucide'\nimport { cn } from '../../lib/utils'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport { icon } from '../../lib/icon'\nimport { autoUpdate, computePosition } from '../../lib/position'\n\nexport type ClipboardState = 'idle' | 'success' | 'error'\n\n// Radix's <Arrow> svg is 10×5, but the popper measures its RENDERED size — the\n// `size-2.5` class makes it 10×10 — and adds that height to sideOffset\n// (measured on the React page: 4 + 10 = 14px gap). Same constants as tooltip.\nconst ARROW_OFFSET = 10\n\n/**\n * <uip-clipboard> — the registry Clipboard (copy button + feedback tooltip) as\n * ONE web component.\n *\n *   <uip-clipboard text=\"npm install @uipkge/ui\" label=\"Copy install\"></uip-clipboard>\n *\n * Button, icon, and label class strings are React's verbatim; the tooltip\n * bubble reuses React's TooltipContent string (the same bubble <uip-tooltip>\n * renders, arrow included). Copies via `navigator.clipboard.writeText` with\n * React's textarea fallback for non-secure contexts, then shows feedback for\n * `timeout` ms (default 2000): the icon swaps to an emerald check and — when\n * `feedback-tooltip` is on (default) — the bubble shows `success-text` /\n * `error-text` instead of `tooltip`. With `feedback-tooltip=\"false\"` the\n * bubble only shows while idle.\n *\n * React's `children` render-prop has no web-component equivalent; the default\n * slot takes static nodes instead, and `data-feedback-state` on the host\n * (`idle` | `success` | `error`) lets page CSS react to the state.\n *\n * Events (bubble, composed): `copy` (detail: { text } — React's `onCopy`,\n * fired before the attempt), `success` (detail: { text }), `error`\n * (detail: { error }).\n *\n * Parts: `base` (the button — React's `className` becomes\n * `class=\"[&::part(base)]:…\"` on the host), `icon`, `label`, `tooltip`.\n */\nexport class UipClipboard extends LitElement {\n  // :host display can't be a utility class (the host has no template of its own).\n  // `contents`: the button keeps its own inline-flex box, like an asChild trigger.\n  static styles = [tailwind, css`:host { display: contents; }`]\n\n  static properties = {\n    text: {},\n    label: {},\n    disabled: { type: Boolean, reflect: true },\n    hideIcon: { type: Boolean, attribute: 'hide-icon' },\n    tooltip: {},\n    successText: { attribute: 'success-text' },\n    errorText: { attribute: 'error-text' },\n    timeout: { type: Number },\n    feedbackTooltip: { type: Boolean, attribute: 'feedback-tooltip', converter: { fromAttribute: (v: string | null) => v !== 'false', toAttribute: () => null } },\n    state: { state: true },\n    tipOpen: { state: true },\n    tipPos: { state: true },\n  }\n\n  text = ''\n  label = ''\n  disabled = false\n  hideIcon = false\n  tooltip = 'Copy'\n  successText = 'Copied!'\n  errorText = 'Failed'\n  timeout = 2000\n  feedbackTooltip = true\n  private state: ClipboardState = 'idle'\n  private tipOpen = false\n  private tipPos: Record<string, string> = {}\n  private resetTimer?: ReturnType<typeof setTimeout>\n  private openTimer?: ReturnType<typeof setTimeout>\n  private hovering = false\n  private stopAutoUpdate?: () => void\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'clipboard')\n    this.setAttribute('data-feedback-state', this.state)\n    this.addEventListener('pointerenter', this.onPointerEnter)\n    this.addEventListener('pointerleave', this.onPointerLeave)\n    this.addEventListener('focusin', this.onFocusIn)\n    this.addEventListener('focusout', this.onFocusOut)\n  }\n\n  disconnectedCallback() {\n    super.disconnectedCallback()\n    this.removeEventListener('pointerenter', this.onPointerEnter)\n    this.removeEventListener('pointerleave', this.onPointerLeave)\n    this.removeEventListener('focusin', this.onFocusIn)\n    this.removeEventListener('focusout', this.onFocusOut)\n    this.teardown()\n    clearTimeout(this.resetTimer)\n    clearTimeout(this.openTimer)\n  }\n\n  private get button() {\n    return this.renderRoot.querySelector<HTMLElement>('[data-slot=\"clipboard\"]')\n  }\n\n  private get bubble() {\n    return this.renderRoot.querySelector<HTMLElement>('[role=tooltip]')\n  }\n\n  private get currentTooltip() {\n    return this.state === 'success' ? this.successText : this.state === 'error' ? this.errorText : this.tooltip\n  }\n\n  protected willUpdate(changed: Map<string, unknown>) {\n    if (changed.has('state')) this.setAttribute('data-feedback-state', this.state)\n  }\n\n  // --- tooltip (Radix Tooltip behaviour, delay 300 like React's provider) ----\n  private get showBubble() {\n    return this.feedbackTooltip || this.state === 'idle'\n  }\n\n  private onPointerEnter = (e: PointerEvent) => {\n    if (e.pointerType === 'touch') return\n    this.hovering = true\n    if (this.disabled || this.tipOpen || !this.showBubble) return\n    clearTimeout(this.openTimer)\n    this.openTimer = setTimeout(() => {\n      if (this.hovering && this.showBubble) this.tipOpen = true\n    }, 300)\n  }\n\n  private onPointerLeave = () => {\n    this.hovering = false\n    clearTimeout(this.openTimer)\n    this.tipOpen = false\n  }\n\n  private onFocusIn = () => {\n    if (this.disabled || !this.showBubble) return\n    this.tipOpen = true\n  }\n\n  private onFocusOut = () => {\n    this.tipOpen = false\n  }\n\n  private onDocKeyDown = (e: KeyboardEvent) => {\n    if (e.key === 'Escape' && this.tipOpen) {\n      e.stopPropagation()\n      this.tipOpen = false\n    }\n  }\n\n  protected updated(changed: Map<string, unknown>) {\n    // React unmounts the bubble during feedback when `feedbackTooltip` is off —\n    // hide it here on state flips too, not just tipOpen changes.\n    if ((!changed.has('tipOpen') && !changed.has('state') && !changed.has('feedbackTooltip')) || isServer) return\n    const bubble = this.bubble\n    if (!bubble) return\n    if (this.tipOpen && this.showBubble) {\n      if (!bubble.matches(':popover-open')) bubble.showPopover()\n      addEventListener('keydown', this.onDocKeyDown, true)\n      this.updateComplete.then(() => {\n        const btn = this.button\n        if (this.tipOpen && btn && !this.stopAutoUpdate) {\n          this.stopAutoUpdate = autoUpdate(btn, bubble, () => this.place())\n          Promise.all(bubble.getAnimations().map((a) => a.finished)).then(() => this.tipOpen && this.place(), () => {})\n        }\n      })\n    } else {\n      this.teardown()\n      if (bubble.matches(':popover-open')) bubble.hidePopover()\n    }\n  }\n\n  private teardown() {\n    this.stopAutoUpdate?.()\n    this.stopAutoUpdate = undefined\n    removeEventListener('keydown', this.onDocKeyDown, true)\n  }\n\n  private place() {\n    const btn = this.button\n    const bubble = this.bubble\n    if (!btn || !bubble) return\n    const { style } = computePosition(btn, bubble, { side: 'top', sideOffset: 4 + ARROW_OFFSET })\n    this.tipPos = style\n  }\n\n  // --- copy ------------------------------------------------------------------\n  private setFeedback(next: ClipboardState) {\n    this.state = next\n    if (this.resetTimer) clearTimeout(this.resetTimer)\n    this.resetTimer = setTimeout(() => (this.state = 'idle'), this.timeout)\n  }\n\n  private async copy() {\n    if (this.disabled) return\n    const value = this.text\n    this.dispatchEvent(new CustomEvent('copy', { detail: { text: value }, bubbles: true, composed: true }))\n    try {\n      if (navigator.clipboard?.writeText) {\n        await navigator.clipboard.writeText(value)\n      } else {\n        // Legacy fallback for non-secure contexts.\n        const ta = document.createElement('textarea')\n        ta.value = value\n        ta.style.position = 'fixed'\n        ta.style.opacity = '0'\n        document.body.appendChild(ta)\n        ta.select()\n        const ok = document.execCommand('copy')\n        document.body.removeChild(ta)\n        if (!ok) throw new Error('execCommand copy failed')\n      }\n      this.setFeedback('success')\n      this.dispatchEvent(new CustomEvent('success', { detail: { text: value }, bubbles: true, composed: true }))\n    } catch (err) {\n      this.setFeedback('error')\n      this.dispatchEvent(new CustomEvent('error', { detail: { error: err }, bubbles: true, composed: true }))\n    }\n  }\n\n  render() {\n    const tip = this.currentTooltip\n    return html`\n      <button\n        part=\"base\"\n        type=\"button\"\n        data-uipkge=\"\"\n        data-slot=\"clipboard\"\n        data-feedback-state=${this.state}\n        ?disabled=${this.disabled}\n        aria-label=${tip}\n        aria-describedby=\"uip-clipboard-tip\"\n        class=${cn(\n          'inline-flex items-center gap-2 rounded-md text-sm transition-colors',\n          'text-muted-foreground hover:text-foreground',\n          'focus-visible:ring-ring/50 outline-none focus-visible:ring-[3px]',\n          'disabled:cursor-not-allowed disabled:opacity-50',\n        )}\n        @click=${() => void this.copy()}\n      >\n        ${!this.hideIcon\n          ? html`<span part=\"icon\" data-slot=\"clipboard-icon\" class=\"inline-flex\">\n              ${this.state === 'success' ? icon(Check, 'check', 'size-4 text-emerald-500') : icon(Copy, 'copy', 'size-4')}\n            </span>`\n          : nothing}\n        ${this.label ? html`<span part=\"label\" data-slot=\"clipboard-label\">${this.label}</span>` : nothing}\n        <slot></slot>\n      </button>\n      <div\n        part=\"tooltip\"\n        id=\"uip-clipboard-tip\"\n        role=\"tooltip\"\n        popover=\"manual\"\n        data-uipkge=\"\"\n        data-slot=\"tooltip-content\"\n        data-state=${this.tipOpen ? 'instant-open' : 'closed'}\n        data-side=\"top\"\n        data-align=\"center\"\n        style=${styleMap(this.tipPos)}\n        class=${cn(\n          'bg-foreground text-background motion-safe:animate-in motion-safe:fade-in-0 motion-safe:zoom-in-95 motion-safe:data-[state=closed]:animate-out motion-safe:blur-in-2 motion-safe:data-[state=closed]:blur-out-2 motion-safe:data-[state=closed]:fade-out-0 motion-safe:data-[state=closed]:zoom-out-95 motion-safe:data-[side=bottom]:slide-in-from-top-2 motion-safe:data-[side=left]:slide-in-from-right-2 motion-safe:data-[side=right]:slide-in-from-left-2 motion-safe:data-[side=top]:slide-in-from-bottom-2 z-50 w-fit rounded-md px-3 py-1.5 text-xs text-balance',\n          'fixed inset-auto m-0 overflow-visible text-start',\n        )}\n      >\n        ${tip}\n        <span style=${styleMap({ position: 'absolute', left: '50%', bottom: '0', transform: 'translate(-50%, 100%)' })}>\n          <svg\n            class=\"bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]\"\n            width=\"10\"\n            height=\"5\"\n            viewBox=\"0 0 30 10\"\n            preserveAspectRatio=\"none\"\n            aria-hidden=\"true\"\n          >\n            <polygon points=\"0,0 30,0 15,10\"></polygon>\n          </svg>\n        </span>\n      </div>\n    `\n  }\n}\n\ncustomElements.get('uip-clipboard') || customElements.define('uip-clipboard', UipClipboard)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-clipboard': UipClipboard\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/clipboard/clipboard.ts"
    }
  ],
  "dependencies": [
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
