{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "signature-pad",
  "title": "Signature Pad",
  "type": "registry:ui",
  "description": "Canvas-based digital signature capture with pointer events (mouse + touch). The value is a PNG data URL, with pen color/thickness, background, clear method, disabled/readonly states, and a live point count.",
  "categories": [
    "control",
    "form"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/signature-pad/signature-pad.ts",
      "content": "import { LitElement, css, html, nothing } from 'lit'\nimport { Eraser } from 'lucide'\nimport { cn } from '../../lib/utils'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport { icon } from '../../lib/icon'\nimport { signaturePadVariants } from './signature-pad.variants'\n\nfunction resolveColor(input: string | undefined, cssVar: string): string {\n  let candidate = (input || '').trim()\n  if (!candidate) candidate = `var(${cssVar})`\n  const hslWrapped = candidate.match(/^hsl\\(\\s*(var\\(--[^)]+\\))\\s*\\)$/i)\n  if (hslWrapped) candidate = hslWrapped[1]!\n  if (candidate.startsWith('var(')) {\n    const name = candidate.match(/var\\((--[^),]+)/)?.[1]\n    if (name && typeof document !== 'undefined') {\n      const v = getComputedStyle(document.documentElement).getPropertyValue(name).trim()\n      if (v) return v\n    }\n  }\n  return candidate\n}\n\nconst trueUnlessFalse = {\n  fromAttribute: (v: string | null) => v !== 'false',\n}\n\n/**\n * <uip-signature-pad> — the registry SignaturePad as a web component.\n *\n * Form-associated: submits `name=dataURL` with its <form>.\n */\nexport class UipSignaturePad extends LitElement {\n  static formAssociated = true\n  static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true }\n  static styles = [tailwind, css`:host { display: inline-block; vertical-align: middle; }`]\n\n  static properties = {\n    value: { reflect: true },\n    width: { type: Number },\n    height: { type: Number },\n    penColor: { attribute: 'pen-color' },\n    penThickness: { type: Number, attribute: 'pen-thickness' },\n    backgroundColor: { attribute: 'background-color' },\n    exportFormat: { attribute: 'export-format' },\n    disabled: { type: Boolean, reflect: true },\n    readOnly: { type: Boolean, attribute: 'readonly', reflect: true },\n    showClearButton: { converter: trueUnlessFalse, attribute: 'show-clear-button' },\n    clearLabel: { attribute: 'clear-label' },\n    name: { reflect: true },\n    pointCount: { state: true },\n    hasInk: { state: true },\n  }\n\n  value: string | null = null\n  width = 400\n  height = 200\n  penColor = ''\n  penThickness = 2\n  backgroundColor = ''\n  exportFormat = 'image/png'\n  disabled = false\n  readOnly = false\n  showClearButton = true\n  clearLabel = 'Clear'\n  name?: string\n\n  pointCount = 0\n  hasInk = false\n\n  private ctx: CanvasRenderingContext2D | null = null\n  private isDrawing = false\n  private lastPos = { x: 0, y: 0 }\n  private internals = this.attachInternals()\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', 'signature-pad')\n  }\n\n  firstUpdated() {\n    this.setupCanvas()\n  }\n\n  updated(changed: Map<string, unknown>) {\n    if (\n      changed.has('width') ||\n      changed.has('height') ||\n      changed.has('penColor') ||\n      changed.has('penThickness') ||\n      changed.has('backgroundColor')\n    ) {\n      this.setupCanvas()\n    }\n  }\n\n  private get canvas(): HTMLCanvasElement | null {\n    return this.renderRoot?.querySelector('canvas')\n  }\n\n  get isEmpty(): boolean {\n    return !this.hasInk\n  }\n\n  private setupCanvas() {\n    const canvas = this.canvas\n    if (!canvas) return\n    const dpr = typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1\n    canvas.width = this.width * dpr\n    canvas.height = this.height * dpr\n    canvas.style.width = `${this.width}px`\n    canvas.style.height = `${this.height}px`\n    const context = canvas.getContext('2d')\n    if (!context) return\n    context.scale(dpr, dpr)\n    context.lineCap = 'round'\n    context.lineJoin = 'round'\n    context.strokeStyle = resolveColor(this.penColor, '--foreground')\n    context.lineWidth = this.penThickness\n    context.fillStyle = resolveColor(this.backgroundColor, '--background')\n    context.fillRect(0, 0, this.width, this.height)\n    this.ctx = context\n    this.pointCount = 0\n    this.hasInk = false\n  }\n\n  formResetCallback() {\n    this.clear()\n  }\n\n  formDisabledCallback(disabled: boolean) {\n    this.disabled = disabled\n  }\n\n  clear = () => {\n    const canvas = this.canvas\n    const context = this.ctx\n    if (!canvas || !context) return\n    context.fillStyle = resolveColor(this.backgroundColor, '--background')\n    context.fillRect(0, 0, this.width, this.height)\n    this.pointCount = 0\n    this.hasInk = false\n    this.value = null\n    this.internals.setFormValue(null)\n    this.dispatchEvent(new Event('input', { bubbles: true, composed: true }))\n    this.dispatchEvent(new Event('change', { bubbles: true, composed: true }))\n    this.dispatchEvent(new CustomEvent('value-change', { detail: { value: null }, bubbles: true, composed: true }))\n  }\n\n  exportSignature = (): string | null => {\n    const canvas = this.canvas\n    if (!canvas) return null\n    if (!this.hasInk) {\n      this.value = null\n      this.internals.setFormValue(null)\n      this.dispatchEvent(new Event('input', { bubbles: true, composed: true }))\n      this.dispatchEvent(new Event('change', { bubbles: true, composed: true }))\n      this.dispatchEvent(new CustomEvent('value-change', { detail: { value: null }, bubbles: true, composed: true }))\n      return null\n    }\n    const dataUrl = canvas.toDataURL(this.exportFormat)\n    this.value = dataUrl\n    this.internals.setFormValue(dataUrl)\n    this.dispatchEvent(new Event('input', { bubbles: true, composed: true }))\n    this.dispatchEvent(new Event('change', { bubbles: true, composed: true }))\n    this.dispatchEvent(new CustomEvent('value-change', { detail: { value: dataUrl }, bubbles: true, composed: true }))\n    return dataUrl\n  }\n\n  toDataURL = (): string | null => {\n    return this.exportSignature()\n  }\n\n  private getPointerPos(e: PointerEvent) {\n    const canvas = this.canvas\n    if (!canvas) return { x: 0, y: 0 }\n    const rect = canvas.getBoundingClientRect()\n    const scaleX = this.width / (rect.width || this.width)\n    const scaleY = this.height / (rect.height || this.height)\n    return {\n      x: (e.clientX - rect.left) * scaleX,\n      y: (e.clientY - rect.top) * scaleY,\n    }\n  }\n\n  private handlePointerDown = (e: PointerEvent) => {\n    if (this.disabled || this.readOnly || !this.ctx) return\n    e.preventDefault()\n    this.isDrawing = true\n    const { x, y } = this.getPointerPos(e)\n    this.lastPos = { x, y }\n    this.ctx.beginPath()\n    this.ctx.moveTo(x, y)\n    this.pointCount++\n    this.hasInk = true\n    this.dispatchEvent(new CustomEvent('begin', { bubbles: true, composed: true }))\n    this.canvas?.setPointerCapture(e.pointerId)\n  }\n\n  private handlePointerMove = (e: PointerEvent) => {\n    if (!this.isDrawing || !this.ctx) return\n    e.preventDefault()\n    const { x, y } = this.getPointerPos(e)\n    this.ctx.beginPath()\n    this.ctx.moveTo(this.lastPos.x, this.lastPos.y)\n    this.ctx.lineTo(x, y)\n    this.ctx.stroke()\n    this.lastPos = { x, y }\n    this.pointCount++\n  }\n\n  private handlePointerUp = (e: PointerEvent) => {\n    if (!this.isDrawing) return\n    this.isDrawing = false\n    this.ctx?.closePath()\n    if (this.canvas?.hasPointerCapture(e.pointerId)) {\n      this.canvas.releasePointerCapture(e.pointerId)\n    }\n    this.exportSignature()\n    this.dispatchEvent(new CustomEvent('end', { bubbles: true, composed: true }))\n  }\n\n  render() {\n    const isInteractive = !this.disabled && !this.readOnly\n\n    return html`\n      <div\n        part=\"base\"\n        data-uipkge=\"\"\n        data-slot=\"signature-pad\"\n        ?data-disabled=${this.disabled}\n        ?data-readonly=${this.readOnly}\n        class=${signaturePadVariants()}\n      >\n        <canvas\n          part=\"canvas\"\n          class=${cn('block touch-none rounded-md', !isInteractive && 'pointer-events-none')}\n          style=\"touch-action: none;\"\n          aria-label=${`Signature pad${this.disabled ? ' (disabled)' : this.readOnly ? ' (readonly)' : ''}`}\n          aria-disabled=${this.disabled ? 'true' : 'false'}\n          role=\"img\"\n          @pointerdown=${this.handlePointerDown}\n          @pointermove=${this.handlePointerMove}\n          @pointerup=${this.handlePointerUp}\n          @pointercancel=${this.handlePointerUp}\n          @pointerleave=${this.handlePointerUp}\n        ></canvas>\n\n        ${this.showClearButton && isInteractive\n          ? html`\n              <div part=\"footer\" class=\"flex items-center justify-between gap-2 pt-2\">\n                <span class=\"text-muted-foreground text-xs tabular-nums\">${this.pointCount} points</span>\n                <button\n                  type=\"button\"\n                  part=\"clear-button\"\n                  class=\"text-muted-foreground hover:text-foreground focus-visible:ring-ring inline-flex items-center gap-1 rounded-md text-xs transition-colors focus-visible:ring-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50\"\n                  ?disabled=${!this.hasInk}\n                  @click=${this.clear}\n                >\n                  ${icon(Eraser, 'eraser', 'size-4')}\n                  ${this.clearLabel}\n                </button>\n              </div>\n            `\n          : nothing}\n\n        <slot name=\"actions\"></slot>\n      </div>\n    `\n  }\n}\n\ncustomElements.get('uip-signature-pad') || customElements.define('uip-signature-pad', UipSignaturePad)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-signature-pad': UipSignaturePad\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/signature-pad/signature-pad.ts"
    },
    {
      "path": "packages/elements-lit/src/components/signature-pad/signature-pad.variants.ts",
      "content": "import type { VariantProps } from 'class-variance-authority'\nimport { cva } from 'class-variance-authority'\n\nexport const signaturePadVariants = cva(\n  'border-input bg-background inline-flex flex-col gap-1 rounded-lg border p-2 shadow-xs',\n)\n\nexport type SignaturePadVariants = VariantProps<typeof signaturePadVariants>\n",
      "type": "registry:file",
      "target": "src/uip/components/signature-pad/signature-pad.variants.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority",
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
