{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "pin-input",
  "title": "Pin Input",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/pin-input/pin-input.component.ts",
      "content": "import {\n  type AfterViewChecked,\n  type AfterViewInit,\n  ChangeDetectorRef,\n  Component,\n  DestroyRef,\n  ElementRef,\n  EventEmitter,\n  Input,\n  Output,\n  ViewChild,\n  ViewEncapsulation,\n  booleanAttribute,\n  computed,\n  effect,\n  forwardRef,\n  inject,\n  numberAttribute,\n  signal,\n  untracked,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { NG_VALUE_ACCESSOR, type ControlValueAccessor } from '@angular/forms'\nimport { cn } from '@/lib/utils'\n\nexport type PinInputStatus = 'error' | 'warning' | 'success' | 'default'\nexport type PinInputSize = 'sm' | 'md' | 'lg'\n\n/** One entry per slot, like input-otp's OTPInputContext `slots`. */\nexport interface PinInputSlotState {\n  char: string | null\n  placeholderChar: string | null\n  isActive: boolean\n  hasFakeCaret: boolean\n}\n\nconst TEXT_ALIGN: Record<'left' | 'center' | 'right', string> = {\n  left: 'text-left',\n  center: 'text-center',\n  right: 'text-right',\n}\n\n/**\n * Angular port of the React PinInput (input-otp). The `<ui-pin-input>` host is input-otp's\n * container (`data-input-otp-container`): the projected groups / slots / separators, then\n * one transparent native <input> stretched over the whole row. The input owns the value,\n * the caret and the selection; slots render `value[index]` and the slot under the caret\n * is `data-active`. Typing, Backspace, arrows and paste are the native input's.\n *\n * Model is React's `value` / `defaultValue` / `valueChange` (React / input-otp `onChange`);\n * `complete` fires once the last slot fills. `status` paints the slots and shakes once\n * when it becomes 'error'; `mask` renders dots. NG_VALUE_ACCESSOR is provided for forms.\n *\n * The keyframes below are React's injected pin-input stylesheet, copied verbatim.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-pin-input',\n  standalone: true,\n  exportAs: 'uiPinInput',\n  encapsulation: ViewEncapsulation.None,\n  providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => UiPinInputComponent), multi: true }],\n  host: {\n    'data-input-otp-container': 'true',\n    '[attr.class]': 'containerClass',\n    '[style.--root-height]': 'rootHeight()',\n    // These belong to the native <input>.\n    '[attr.id]': 'null',\n    '[attr.aria-label]': 'null',\n  },\n  styles: [\n    `\n      @keyframes pin-slot-pop {\n        0% {\n          transform: scale(1);\n        }\n        40% {\n          transform: scale(1.06);\n        }\n        100% {\n          transform: scale(1);\n        }\n      }\n      @keyframes pin-char-pop {\n        0% {\n          opacity: 0.55;\n          transform: scale(0.88);\n        }\n        55% {\n          opacity: 1;\n          transform: scale(1.06);\n        }\n        100% {\n          opacity: 1;\n          transform: scale(1);\n        }\n      }\n      @keyframes pin-input-shake {\n        0%,\n        100% {\n          transform: translateX(0);\n        }\n        20% {\n          transform: translateX(-5px);\n        }\n        40% {\n          transform: translateX(5px);\n        }\n        60% {\n          transform: translateX(-3px);\n        }\n        80% {\n          transform: translateX(3px);\n        }\n      }\n      [data-slot='pin-input-slot'].pin-slot-pop {\n        animation: pin-slot-pop 200ms cubic-bezier(0.22, 1.25, 0.36, 1) both;\n        z-index: 1;\n      }\n      [data-slot='pin-input-slot'] .pin-char-pop {\n        display: inline-block;\n        transform-origin: center center;\n        animation: pin-char-pop 200ms cubic-bezier(0.22, 1.25, 0.36, 1) both;\n      }\n      /* Class may live on OTPInput's container (no data-slot) or a root wrapper. */\n      .pin-input-shake {\n        animation: pin-input-shake 380ms cubic-bezier(0.36, 0.07, 0.19, 0.97) both;\n      }\n      @media (prefers-reduced-motion: reduce) {\n        [data-slot='pin-input-slot'].pin-slot-pop,\n        [data-slot='pin-input-slot'] .pin-char-pop,\n        .pin-input-shake {\n          animation: none !important;\n        }\n        [data-slot='pin-input-slot'] {\n          transition-duration: 0ms !important;\n        }\n      }\n    `,\n  ],\n  template: `\n    <ng-content />\n    <div class=\"pointer-events-none absolute inset-0\">\n      <input\n        #inputEl\n        [attr.autocomplete]=\"autoComplete\"\n        [attr.id]=\"id ?? null\"\n        [attr.name]=\"name ?? null\"\n        [attr.aria-label]=\"ariaLabel ?? null\"\n        data-uipkge=\"\"\n        data-slot=\"pin-input\"\n        [attr.data-status]=\"status === 'default' ? null : status\"\n        data-input-otp=\"true\"\n        [attr.data-input-otp-placeholder-shown]=\"currentValue().length === 0 ? 'true' : null\"\n        [attr.data-input-otp-mss]=\"mss()\"\n        [attr.data-input-otp-mse]=\"mse()\"\n        [attr.inputmode]=\"inputMode\"\n        [attr.pattern]=\"patternSource\"\n        [attr.aria-placeholder]=\"placeholder ?? null\"\n        [attr.maxlength]=\"maxLength\"\n        [value]=\"currentValue()\"\n        [disabled]=\"disabled\"\n        [readOnly]=\"readOnly\"\n        [required]=\"required\"\n        [autofocus]=\"autoFocus\"\n        [class]=\"inputClass\"\n        (paste)=\"handlePaste($event)\"\n        (input)=\"handleChange($event)\"\n        (focus)=\"handleFocus($event)\"\n        (blur)=\"handleBlur($event)\"\n      />\n    </div>\n  `,\n})\nexport class UiPinInputComponent implements ControlValueAccessor, AfterViewInit, AfterViewChecked {\n  // Zoneless-safe: form writes happen outside template events, so schedule a repaint.\n  private readonly cdr = inject(ChangeDetectorRef)\n  private readonly destroyRef = inject(DestroyRef)\n  @ViewChild('inputEl', { static: true }) inputEl!: ElementRef<HTMLInputElement>\n\n  private readonly _valueProp = signal<string | undefined>(undefined)\n  private readonly _internal = signal<string | null>(null)\n  private readonly _status = signal<PinInputStatus>('default')\n  private readonly _maxLength = signal(6)\n\n  /** Controlled value (React `value`). Reading it returns the resolved current value. */\n  @Input()\n  set value(v: string | null | undefined) {\n    this._valueProp.set(v == null ? undefined : String(v))\n  }\n  get value(): string {\n    return this.currentValue()\n  }\n  @Input() defaultValue?: string\n  /** React / input-otp `onChange`: the joined string on every edit. */\n  @Output() valueChange = new EventEmitter<string>()\n  /** React `onComplete`: the joined string once every slot is filled. */\n  @Output() complete = new EventEmitter<string>()\n  @Output('focus') focusEvent = new EventEmitter<FocusEvent>()\n  @Output('blur') blurEvent = new EventEmitter<FocusEvent>()\n\n  /** Number of slots (input-otp `maxLength`). */\n  @Input({ transform: numberAttribute })\n  set maxLength(v: number) {\n    this._maxLength.set(v)\n  }\n  get maxLength(): number {\n    return this._maxLength()\n  }\n  /** Render the typed characters as dots instead of plain text. */\n  @Input({ transform: booleanAttribute }) mask = false\n  @Input()\n  set status(v: PinInputStatus) {\n    this._status.set(v ?? 'default')\n  }\n  get status(): PinInputStatus {\n    return this._status()\n  }\n  @Input() size: PinInputSize = 'md'\n  @Input({ transform: booleanAttribute }) disabled = false\n  @Input({ transform: booleanAttribute }) readOnly = false\n  @Input({ transform: booleanAttribute }) required = false\n  @Input({ transform: booleanAttribute }) autoFocus = false\n  /** Only accept values matching this pattern (input-otp REGEXP_ONLY_DIGITS etc.). */\n  @Input() pattern?: string | RegExp\n  @Input() inputMode = 'numeric'\n  @Input() placeholder?: string\n  @Input() textAlign: 'left' | 'center' | 'right' = 'left'\n  @Input() pasteTransformer?: (pasted: string) => string\n  @Input() autoComplete = 'one-time-code'\n  @Input() id?: string\n  @Input() name?: string\n  @Input('aria-label') ariaLabel?: string\n  /** React `className`: lands on the native input. */\n  @Input('class') className?: string\n  @Input() containerClassName?: string\n\n  readonly currentValue = computed(() => this._valueProp() ?? this._internal() ?? this.defaultValue ?? '')\n  readonly isFocused = signal(false)\n  readonly mss = signal<number | null>(null)\n  readonly mse = signal<number | null>(null)\n  readonly rootHeight = signal<string | null>(null)\n  readonly isShaking = signal(false)\n\n  /** input-otp's slot model, read by every <ui-pin-input-slot>. */\n  readonly slots = computed<PinInputSlotState[]>(() => {\n    const v = this.currentValue()\n    const focused = this.isFocused()\n    const start = this.mss()\n    const end = this.mse()\n    return Array.from({ length: this._maxLength() }, (_, o) => {\n      const isActive =\n        focused && start !== null && end !== null && ((start === end && o === start) || (o >= start && o < end))\n      const char = v[o] !== undefined ? v[o]! : null\n      const placeholderChar = v[0] !== undefined ? null : (this.placeholder?.[o] ?? null)\n      return { char, placeholderChar, isActive, hasFakeCaret: isActive && char === null }\n    })\n  })\n\n  private prevSelection: [number | null, number | null, string | null] = [null, null, null]\n  private readonly timers = new Set<ReturnType<typeof setTimeout>>()\n  private onChange: (v: string) => void = () => {}\n  private onTouched: () => void = () => {}\n\n  constructor() {\n    // onComplete: only on the edit that fills the last slot.\n    let prev: string | undefined\n    effect(() => {\n      const v = this.currentValue()\n      const max = this._maxLength()\n      untracked(() => {\n        if (prev !== undefined && v !== prev && prev.length < max && v.length === max) this.complete.emit(v)\n        prev = v\n      })\n    })\n    // One-shot shake when status transitions into error (not on mount).\n    let prevStatus: PinInputStatus | null = null\n    let frame = 0\n    let timer: ReturnType<typeof setTimeout> | undefined\n    effect(() => {\n      const status = this._status()\n      untracked(() => {\n        if (prevStatus !== null && status === 'error' && prevStatus !== 'error') {\n          this.isShaking.set(false)\n          cancelAnimationFrame(frame)\n          clearTimeout(timer)\n          // Restart the animation on the next frame; clear it once it has run.\n          frame = requestAnimationFrame(() => {\n            this.isShaking.set(true)\n            timer = setTimeout(() => this.isShaking.set(false), 400)\n          })\n        }\n        prevStatus = status\n      })\n    })\n    this.destroyRef.onDestroy(() => {\n      cancelAnimationFrame(frame)\n      clearTimeout(timer)\n      for (const id of this.timers) clearTimeout(id)\n    })\n  }\n\n  get patternSource(): string | null {\n    const p = this.pattern\n    return p ? (typeof p === 'string' ? p : p.source) : null\n  }\n\n  get containerClass(): string {\n    return cn(\n      // input-otp's container inline styles, as utilities.\n      'relative select-none pointer-events-none',\n      this.disabled ? 'cursor-default' : 'cursor-text',\n      'flex items-center gap-2 has-disabled:opacity-50',\n      this.isShaking() && 'pin-input-shake',\n      this.containerClassName,\n    )\n  }\n\n  get inputClass(): string {\n    return cn(\n      // input-otp's input inline styles + injected stylesheet, as utilities.\n      'absolute inset-0 flex h-full w-full bg-transparent leading-none tabular-nums text-transparent caret-transparent opacity-100 shadow-none pointer-events-auto',\n      'border-0 border-solid border-transparent outline-0 outline-solid outline-transparent',\n      'tracking-[-.5em] text-[length:var(--root-height)] font-[monospace]',\n      TEXT_ALIGN[this.textAlign],\n      'selection:bg-transparent selection:text-transparent',\n      'autofill:bg-transparent! autofill:text-transparent! autofill:border-transparent! autofill:opacity-0! autofill:shadow-none! autofill:[-webkit-text-fill-color:transparent]!',\n      'disabled:cursor-not-allowed',\n      this.className,\n    )\n  }\n\n  private get regex(): RegExp | null {\n    const p = this.pattern\n    return p ? (typeof p === 'string' ? new RegExp(p) : p) : null\n  }\n\n  ngAfterViewInit(): void {\n    const input = this.inputEl.nativeElement\n    this.prevSelection = [input.selectionStart, input.selectionEnd, input.selectionDirection]\n    const onSelection = () => this.syncSelection()\n    document.addEventListener('selectionchange', onSelection, { capture: true })\n    this.syncSelection()\n    if (document.activeElement === input) this.isFocused.set(true)\n    const measure = () => this.rootHeight.set(`${input.clientHeight}px`)\n    measure()\n    const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(measure) : null\n    ro?.observe(input)\n    this.destroyRef.onDestroy(() => {\n      document.removeEventListener('selectionchange', onSelection, { capture: true })\n      ro?.disconnect()\n    })\n  }\n\n  /** The [value] binding misses a rejected edit (pattern / controlled parent): sync the DOM. */\n  ngAfterViewChecked(): void {\n    const el = this.inputEl?.nativeElement\n    if (el && el.value !== this.currentValue()) el.value = this.currentValue()\n  }\n\n  /** input-otp's selectionchange handler: keep a one-char selection so the caret \"is\" a slot. */\n  syncSelection(): void {\n    const t = this.inputEl?.nativeElement\n    if (!t) return\n    if (document.activeElement !== t) {\n      this.mss.set(null)\n      this.mse.set(null)\n      return\n    }\n    const c = t.selectionStart\n    const b = t.selectionEnd\n    const dir = t.selectionDirection\n    const v = t.maxLength\n    const C = t.value\n    const prev = this.prevSelection\n    let g = -1\n    let E = -1\n    let w: 'forward' | 'backward' | undefined\n    if (C.length !== 0 && c !== null && b !== null) {\n      const collapsed = c === b\n      const atEnd = c === C.length && C.length < v\n      if (collapsed && !atEnd) {\n        const y = c\n        if (y === 0) {\n          g = 0\n          E = 1\n          w = 'forward'\n        } else if (y === v) {\n          g = y - 1\n          E = y\n          w = 'backward'\n        } else if (v > 1 && C.length > 1) {\n          let et = 0\n          if (prev[0] !== null && prev[1] !== null) {\n            w = y < prev[1] ? 'backward' : 'forward'\n            const wasCollapsed = prev[0] === prev[1] && prev[0] < v\n            if (w === 'backward' && !wasCollapsed) et = -1\n          }\n          g = et + y\n          E = et + y + 1\n        }\n      }\n      if (g !== -1 && E !== -1 && g !== E) t.setSelectionRange(g, E, w)\n    }\n    const start = g !== -1 ? g : c\n    const end = E !== -1 ? E : b\n    this.mss.set(start)\n    this.mse.set(end)\n    this.prevSelection = [start, end, w ?? dir]\n  }\n\n  /** input-otp re-reads the selection a few times after the value / focus changes. */\n  private resyncSoon(): void {\n    const read = () => {\n      const t = this.inputEl?.nativeElement\n      if (!t) return\n      const s = t.selectionStart\n      const e = t.selectionEnd\n      if (s !== null && e !== null && document.activeElement === t) {\n        this.mss.set(s)\n        this.mse.set(e)\n        this.prevSelection = [s, e, t.selectionDirection]\n      }\n    }\n    for (const ms of [0, 10, 50]) {\n      const id = setTimeout(() => {\n        this.timers.delete(id)\n        read()\n      }, ms)\n      this.timers.add(id)\n    }\n  }\n\n  private setValue(next: string): void {\n    this._internal.set(next)\n    this.onChange(next)\n    this.valueChange.emit(next)\n  }\n\n  handleChange(event: Event): void {\n    const target = event.target as HTMLInputElement\n    const next = target.value.slice(0, this.maxLength)\n    const re = this.regex\n    if (next.length > 0 && re && !re.test(next)) return\n    const prev = this.currentValue()\n    if (next.length < prev.length) document.dispatchEvent(new Event('selectionchange'))\n    this.setValue(next)\n    this.resyncSoon()\n  }\n\n  handleFocus(event: FocusEvent): void {\n    const t = this.inputEl.nativeElement\n    const start = Math.min(t.value.length, this.maxLength - 1)\n    const end = t.value.length\n    t.setSelectionRange(start, end)\n    this.mss.set(start)\n    this.mse.set(end)\n    this.isFocused.set(true)\n    this.resyncSoon()\n    this.focusEvent.emit(event)\n  }\n\n  handleBlur(event: FocusEvent): void {\n    this.isFocused.set(false)\n    this.onTouched()\n    this.blurEvent.emit(event)\n  }\n\n  /** input-otp only takes over paste with a pasteTransformer (and on iOS). */\n  handlePaste(event: ClipboardEvent): void {\n    const t = this.inputEl.nativeElement\n    if (!this.pasteTransformer || !event.clipboardData) return\n    const raw = event.clipboardData.getData('text/plain')\n    const content = this.pasteTransformer(raw)\n    event.preventDefault()\n    const v = this.currentValue()\n    const s = t.selectionStart ?? 0\n    const e = t.selectionEnd ?? s\n    const next = (s !== e ? v.slice(0, s) + content + v.slice(e) : v.slice(0, s) + content + v.slice(s)).slice(\n      0,\n      this.maxLength,\n    )\n    const re = this.regex\n    if (next.length > 0 && re && !re.test(next)) return\n    t.value = next\n    this.setValue(next)\n    const start = Math.min(next.length, this.maxLength - 1)\n    t.setSelectionRange(start, next.length)\n    this.mss.set(start)\n    this.mse.set(next.length)\n  }\n\n  /** Focus the hidden input (React: the forwarded ref). */\n  focus(): void {\n    this.inputEl?.nativeElement.focus()\n  }\n\n  writeValue(v: string | null): void {\n    this._internal.set(v ?? '')\n    this.cdr.markForCheck()\n  }\n\n  registerOnChange(fn: (v: string) => void): void {\n    this.onChange = fn\n  }\n\n  registerOnTouched(fn: () => void): void {\n    this.onTouched = fn\n  }\n\n  setDisabledState(isDisabled: boolean): void {\n    this.disabled = isDisabled\n    this.cdr.markForCheck()\n  }\n}\n\n/** React PinInputGroup: a flush row of slots. */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-pin-input-group',\n  standalone: true,\n  host: {\n    'data-uipkge': '',\n    'data-slot': 'pin-input-group',\n    '[class]': 'hostClass',\n  },\n  template: `<ng-content />`,\n})\nexport class UiPinInputGroupComponent {\n  @Input('class') className?: string\n\n  get hostClass(): string {\n    return cn('flex items-center', this.className)\n  }\n}\n\nconst sizeClassMap: Record<PinInputSize, string> = {\n  sm: 'h-8 w-8 text-sm',\n  lg: 'h-12 w-12 text-xl',\n  md: 'h-10 w-10 text-base',\n}\n\nconst statusClassMap: Record<PinInputStatus, string> = {\n  error: 'border-destructive focus-within:border-destructive focus-within:ring-destructive/40 text-destructive',\n  warning: 'border-warning focus-within:border-warning focus-within:ring-warning/40 text-warning',\n  success: 'border-success focus-within:border-success focus-within:ring-success/40 text-success',\n  default: '',\n}\n\n/** React PinInputSlot: renders `value[index]` (or a dot when masked); pops when a char lands. */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-pin-input-slot',\n  standalone: true,\n  host: {\n    'data-uipkge': '',\n    'data-slot': 'pin-input-slot',\n    '[attr.data-active]': 'slot()?.isActive ? \"\" : null',\n    '[class]': 'hostClass',\n    '(animationend)': 'onAnimationEnd($event)',\n  },\n  template: `\n    @if (slot()?.char != null) {\n      @for (gen of [popGen()]; track gen) {\n        @if (effectiveMask) {\n          <span [class]=\"maskClass\"></span>\n        } @else {\n          <span [class]=\"gen > 0 ? 'pin-char-pop' : ''\">{{ slot()!.char }}</span>\n        }\n      }\n    }\n  `,\n})\nexport class UiPinInputSlotComponent {\n  private readonly root = inject(UiPinInputComponent)\n  private readonly _index = signal(0)\n\n  @Input({ required: true, transform: numberAttribute })\n  set index(v: number) {\n    this._index.set(v)\n  }\n  get index(): number {\n    return this._index()\n  }\n  /** Override the inherited mask flag for this slot. */\n  @Input({ transform: (v: unknown) => (v === undefined || v === null ? undefined : booleanAttribute(v)) })\n  mask?: boolean\n  @Input('class') className?: string\n\n  readonly slot = computed<PinInputSlotState | undefined>(() => this.root.slots()[this._index()])\n  readonly popGen = signal(0)\n  readonly isPopping = signal(false)\n\n  constructor() {\n    // Quiet pop when a character lands (including paste into this slot); not on mount.\n    let first = true\n    let prevChar: string | null = null\n    effect(() => {\n      const char = this.slot()?.char ?? null\n      untracked(() => {\n        if (first) {\n          first = false\n          prevChar = char\n          return\n        }\n        if (char != null && char !== '' && char !== prevChar) {\n          this.popGen.update((g) => g + 1)\n          this.isPopping.set(true)\n        }\n        prevChar = char\n      })\n    })\n  }\n\n  get effectiveMask(): boolean {\n    return this.mask ?? this.root.mask\n  }\n\n  get maskClass(): string {\n    return cn('bg-foreground size-2 rounded-full', this.popGen() > 0 && 'pin-char-pop')\n  }\n\n  get hostClass(): string {\n    return cn(\n      'border-input bg-background text-foreground relative -ml-px flex items-center justify-center border text-center shadow-xs outline-none first:ml-0 first:rounded-l-md last:rounded-r-md',\n      'transition-[border-color,box-shadow,color,transform] duration-150 ease-out',\n      'focus-within:border-ring focus-within:ring-ring/40 focus-within:relative focus-within:z-10 focus-within:ring-2',\n      'disabled:cursor-not-allowed disabled:opacity-50',\n      this.slot()?.isActive && 'border-ring ring-ring/40 z-10 ring-2',\n      this.isPopping() && 'pin-slot-pop',\n      sizeClassMap[this.root.size],\n      statusClassMap[this.root.status],\n      this.className,\n    )\n  }\n\n  /** Only clear on the slot animation -- the child char-pop bubbles first. */\n  onAnimationEnd(event: AnimationEvent): void {\n    if (event.target === event.currentTarget && event.animationName === 'pin-slot-pop') this.isPopping.set(false)\n  }\n}\n\n/** React PinInputSeparator: a Minus icon (or projected content) between groups. */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-pin-input-separator',\n  standalone: true,\n  host: {\n    'data-uipkge': '',\n    'data-slot': 'pin-input-separator',\n    role: 'separator',\n    class: 'block',\n  },\n  template: `<ng-content\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-minus\"\n      aria-hidden=\"true\"\n    >\n      <path d=\"M5 12h14\" /></svg\n  ></ng-content>`,\n})\nexport class UiPinInputSeparatorComponent {}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/pin-input/pin-input.component.ts"
    },
    {
      "path": "packages/registry-angular/components/pin-input/index.ts",
      "content": "export {\n  UiPinInputComponent,\n  UiPinInputGroupComponent,\n  UiPinInputSlotComponent,\n  UiPinInputSeparatorComponent,\n  type PinInputStatus,\n  type PinInputSize,\n  type PinInputSlotState,\n} from './pin-input.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/pin-input/index.ts"
    }
  ],
  "dependencies": [
    "@angular/forms"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "One-time-code input — N separate boxes that auto-advance and accept paste. Use for SMS verification, 2FA, and short numeric codes. Length, masking, and per-slot status all configurable.",
  "categories": [
    "form"
  ]
}