{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "signature-pad",
  "title": "Signature Pad",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/signature-pad/signature-pad.component.ts",
      "content": "import {\n  AfterViewInit,\n  ChangeDetectorRef,\n  Component,\n  Directive,\n  ElementRef,\n  EventEmitter,\n  Input,\n  OnChanges,\n  OnDestroy,\n  Output,\n  SimpleChanges,\n  TemplateRef,\n  ViewChild,\n  ViewContainerRef,\n  booleanAttribute,\n  forwardRef,\n  inject,\n  numberAttribute,\n  signal,\n  type EmbeddedViewRef,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { NG_VALUE_ACCESSOR, type ControlValueAccessor } from '@angular/forms'\nimport { cn } from '@/lib/utils'\nimport { signaturePadVariants, type SignaturePadVariants } from './signature-pad.variants'\n\n/** Context of the `actions` template (React `actions({ clear, exportSignature, empty })`). */\nexport interface SignaturePadActionsContext {\n  $implicit: SignaturePadActionsState\n  clear: () => void\n  exportSignature: () => void\n  empty: boolean\n}\nexport interface SignaturePadActionsState {\n  clear: () => void\n  exportSignature: () => void\n  empty: boolean\n}\n\n/** Resolve theme CSS variables (and legacy `hsl(var(--x))`) to a paint color canvas accepts. */\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\n/** Renders the `actions` template, keeping one view and refreshing its context. */\n@Directive({ selector: '[uiSignaturePadActions]', standalone: true })\nexport class UiSignaturePadActionsDirective implements OnChanges, OnDestroy {\n  @Input('uiSignaturePadActions') template!: TemplateRef<SignaturePadActionsContext>\n  @Input('uiSignaturePadActionsState') state!: SignaturePadActionsState\n  private readonly vcr = inject(ViewContainerRef)\n  private view?: EmbeddedViewRef<SignaturePadActionsContext>\n\n  ngOnChanges(changes: SimpleChanges): void {\n    const context = { $implicit: this.state, ...this.state }\n    if (changes['template'] || !this.view) {\n      this.vcr.clear()\n      this.view = this.vcr.createEmbeddedView(this.template, context)\n    } else {\n      Object.assign(this.view.context, context)\n    }\n  }\n\n  ngOnDestroy(): void {\n    this.vcr.clear()\n  }\n}\n\n/**\n * Angular port of the React SignaturePad: a DPR-aware canvas that strokes pointer input\n * (mouse / touch / pen, with pointer capture), a footer with the live point count and a\n * Clear button, and an optional `actions` template. The value is a PNG data URL (null when\n * empty): `modelValue` in, `modelChange` / `change` out (React `onModelChange` /\n * `onChange`; `modelValueChange` too, so `[(modelValue)]` and forms work), plus\n * `begin` / `end` per stroke. React's ref handle is the public API: `clear()`,\n * `exportSignature()`, `toDataURL()`, `pointCount`, `isEmpty`.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-signature-pad, [ui-signature-pad]',\n  standalone: true,\n  imports: [UiSignaturePadActionsDirective],\n  exportAs: 'uiSignaturePad',\n  providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => UiSignaturePadComponent), multi: true }],\n  host: {\n    'data-uipkge': '',\n    'data-slot': 'signature-pad',\n    '[attr.data-disabled]': 'disabled ? \"\" : null',\n    '[attr.data-readonly]': 'readonly ? \"\" : null',\n    '[class]': 'hostClass',\n  },\n  template: `\n    <canvas\n      #canvas\n      [class]=\"canvasClass\"\n      [attr.aria-label]=\"ariaLabel\"\n      [attr.aria-disabled]=\"disabled || null\"\n      role=\"img\"\n      (pointerdown)=\"onPointerDown($event)\"\n      (pointermove)=\"onPointerMove($event)\"\n      (pointerup)=\"onPointerUp($event)\"\n      (pointercancel)=\"onPointerUp($event)\"\n      (pointerleave)=\"onPointerUp($event)\"\n    ></canvas>\n    @if (showClearButton && isInteractive) {\n      <div class=\"flex items-center justify-between gap-2 pt-2\">\n        <span class=\"text-muted-foreground text-xs tabular-nums\">{{ pointCount }} points</span>\n        <button\n          type=\"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]=\"!hasInk()\"\n          (click)=\"clear()\"\n        >\n          <svg\n            xmlns=\"http://www.w3.org/2000/svg\"\n            width=\"24\"\n            height=\"24\"\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            stroke-width=\"2\"\n            stroke-linecap=\"round\"\n            stroke-linejoin=\"round\"\n            class=\"lucide lucide-eraser size-4\"\n            aria-hidden=\"true\"\n          >\n            <path\n              d=\"M21 21H8a2 2 0 0 1-1.42-.587l-3.994-3.999a2 2 0 0 1 0-2.828l10-10a2 2 0 0 1 2.829 0l5.999 6a2 2 0 0 1 0 2.828L12.834 21\"\n            />\n            <path d=\"m5.082 11.09 8.828 8.828\" />\n          </svg>\n          {{ clearLabel }}\n        </button>\n      </div>\n    }\n    @if (actions) {\n      <ng-container [uiSignaturePadActions]=\"actions\" [uiSignaturePadActionsState]=\"actionsState\" />\n    }\n  `,\n})\nexport class UiSignaturePadComponent implements ControlValueAccessor, AfterViewInit, OnChanges {\n  // Zoneless-safe: form writes happen outside template events, so schedule a repaint.\n  private readonly cdr = inject(ChangeDetectorRef)\n  private readonly _pointCount = signal(0)\n  readonly hasInk = signal(false)\n  private ctx: CanvasRenderingContext2D | null = null\n  private drawing = false\n  private lastPos = { x: 0, y: 0 }\n  private viewReady = false\n  private onChangeFn: (v: string | null) => void = () => {}\n  private onTouched: () => void = () => {}\n  private _actionsState?: SignaturePadActionsState\n\n  /** PNG data URL of the current canvas contents, or null when empty (read-only from outside). */\n  @Input() modelValue?: string | null\n  @Input({ transform: numberAttribute }) width = 400\n  @Input({ transform: numberAttribute }) height = 200\n  /** Empty resolves --foreground (theme-aware). */\n  @Input() penColor = ''\n  @Input({ transform: numberAttribute }) penThickness = 2\n  /** Empty resolves --background (theme-aware). */\n  @Input() backgroundColor = ''\n  @Input() exportFormat = 'image/png'\n  @Input({ transform: booleanAttribute }) disabled = false\n  @Input({ transform: booleanAttribute }) readonly = false\n  @Input({ transform: booleanAttribute }) showClearButton = true\n  @Input() clearLabel = 'Clear'\n  @Input('class') className?: string\n  /** Custom action controls (`<ng-template let-clear=\"clear\" let-empty=\"empty\">`). */\n  @Input() actions?: TemplateRef<SignaturePadActionsContext>\n\n  /** Fired with the new data URL (or null) whenever the signature changes (React `onModelChange`). */\n  @Output() modelChange = new EventEmitter<string | null>()\n  /** Same payload as `modelChange`, named for `[(modelValue)]`. */\n  @Output() modelValueChange = new EventEmitter<string | null>()\n  /** Same payload as `modelChange` (React `onChange`). */\n  @Output() change = new EventEmitter<string | null>()\n  /** A stroke began. */\n  @Output() begin = new EventEmitter<void>()\n  /** A stroke ended. */\n  @Output() end = new EventEmitter<void>()\n\n  @ViewChild('canvas', { static: true }) canvasRef!: ElementRef<HTMLCanvasElement>\n\n  get pointCount(): number {\n    return this._pointCount()\n  }\n\n  get isEmpty(): boolean {\n    return !this.hasInk()\n  }\n\n  get isInteractive(): boolean {\n    return !this.disabled && !this.readonly\n  }\n\n  get hostClass(): string {\n    return cn(signaturePadVariants(), this.className)\n  }\n\n  get canvasClass(): string {\n    return cn('block touch-none rounded-md', !this.isInteractive && 'pointer-events-none')\n  }\n\n  get ariaLabel(): string {\n    return `Signature pad${this.disabled ? ' (disabled)' : this.readonly ? ' (readonly)' : ''}`\n  }\n\n  /** Stable object while `empty` is unchanged, so the actions view only refreshes on change. */\n  get actionsState(): SignaturePadActionsState {\n    const empty = !this.hasInk()\n    if (!this._actionsState || this._actionsState.empty !== empty) {\n      this._actionsState = { clear: () => this.clear(), exportSignature: () => this.exportSignature(), empty }\n    }\n    return this._actionsState\n  }\n\n  ngAfterViewInit(): void {\n    this.viewReady = true\n    this.setupCanvas()\n  }\n\n  ngOnChanges(changes: SimpleChanges): void {\n    // React re-runs the canvas setup (which resets the ink) when these change.\n    const setupKeys = ['width', 'height', 'penColor', 'penThickness', 'backgroundColor']\n    if (this.viewReady && setupKeys.some((k) => changes[k])) this.setupCanvas()\n  }\n\n  private setupCanvas(): void {\n    const canvas = this.canvasRef.nativeElement\n    const dpr = window.devicePixelRatio || 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.set(0)\n    this.hasInk.set(false)\n  }\n\n  private pointerPos(e: PointerEvent): { x: number; y: number } {\n    const rect = this.canvasRef.nativeElement.getBoundingClientRect()\n    // Map CSS pixels → logical canvas coords if the element is CSS-scaled.\n    const scaleX = this.width / (rect.width || this.width)\n    const scaleY = this.height / (rect.height || this.height)\n    return { x: (e.clientX - rect.left) * scaleX, y: (e.clientY - rect.top) * scaleY }\n  }\n\n  private emitValue(value: string | null): void {\n    this.modelChange.emit(value)\n    this.modelValueChange.emit(value)\n    this.change.emit(value)\n    this.onChangeFn(value)\n  }\n\n  /** Emit the current data URL (or null when empty) through modelChange / change. */\n  exportSignature(): void {\n    if (!this.hasInk()) {\n      this.emitValue(null)\n      return\n    }\n    this.emitValue(this.canvasRef.nativeElement.toDataURL(this.exportFormat))\n  }\n\n  /** Alias of exportSignature (React ref `toDataURL`). */\n  toDataURL(): void {\n    this.exportSignature()\n  }\n\n  /** Repaint the background, reset the point count and emit null. */\n  clear(): void {\n    const context = this.ctx\n    if (!context) return\n    context.fillStyle = resolveColor(this.backgroundColor, '--background')\n    context.fillRect(0, 0, this.width, this.height)\n    this._pointCount.set(0)\n    this.hasInk.set(false)\n    this.emitValue(null)\n  }\n\n  onPointerDown(e: PointerEvent): void {\n    if (!this.isInteractive || !this.ctx) return\n    e.preventDefault()\n    this.drawing = true\n    const { x, y } = this.pointerPos(e)\n    this.lastPos = { x, y }\n    this.ctx.beginPath()\n    this.ctx.moveTo(x, y)\n    this._pointCount.update((c) => c + 1)\n    this.hasInk.set(true)\n    this.begin.emit()\n    this.canvasRef.nativeElement.setPointerCapture?.(e.pointerId)\n  }\n\n  onPointerMove(e: PointerEvent): void {\n    if (!this.drawing || !this.ctx) return\n    e.preventDefault()\n    const { x, y } = this.pointerPos(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.update((c) => c + 1)\n  }\n\n  onPointerUp(e: PointerEvent): void {\n    if (!this.drawing) return\n    this.drawing = false\n    this.ctx?.closePath()\n    this.canvasRef.nativeElement.releasePointerCapture?.(e.pointerId)\n    this.exportSignature()\n    this.end.emit()\n    this.onTouched()\n  }\n\n  // ── ControlValueAccessor (the value is output-only; like React it is never painted back) ──\n  writeValue(v: string | null): void {\n    this.modelValue = v\n    this.cdr.markForCheck()\n  }\n  registerOnChange(fn: (v: string | null) => void): void {\n    this.onChangeFn = fn\n  }\n  registerOnTouched(fn: () => void): void {\n    this.onTouched = fn\n  }\n  setDisabledState(isDisabled: boolean): void {\n    this.disabled = isDisabled\n    this.cdr.markForCheck()\n  }\n}\n\nexport { signaturePadVariants, type SignaturePadVariants }\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/signature-pad/signature-pad.component.ts"
    },
    {
      "path": "packages/registry-angular/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:ui",
      "target": "~/src/app/components/ui/signature-pad/signature-pad.variants.ts"
    },
    {
      "path": "packages/registry-angular/components/signature-pad/index.ts",
      "content": "export {\n  UiSignaturePadComponent,\n  signaturePadVariants,\n  type SignaturePadVariants,\n  type SignaturePadActionsContext,\n  type SignaturePadActionsState,\n} from './signature-pad.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/signature-pad/index.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority",
    "@angular/forms"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Canvas-based digital signature capture with pointer events (mouse + touch). The value is a PNG data URL (modelValue / modelChange, form control), with pen color/thickness, background, clear / export methods, an actions template, disabled/readonly states, and a live point count.",
  "categories": [
    "control",
    "form"
  ]
}