{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "password-input",
  "title": "Password Input",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/password-input/password-input.component.ts",
      "content": "import {\n  type AfterViewChecked,\n  ChangeDetectorRef,\n  Component,\n  ElementRef,\n  EventEmitter,\n  Input,\n  Output,\n  ViewChild,\n  booleanAttribute,\n  forwardRef,\n  inject,\n  numberAttribute,\n  signal,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { NG_VALUE_ACCESSOR, type ControlValueAccessor } from '@angular/forms'\nimport { cn } from '@/lib/utils'\nimport { passwordInputVariants, type PasswordInputVariants } from './password-input.variants'\n\nexport type PasswordInputSize = NonNullable<PasswordInputVariants['size']>\nexport type PasswordInputVariant = NonNullable<PasswordInputVariants['variant']>\nexport type PasswordStrength = 'weak' | 'fair' | 'good' | 'strong'\n\nexport interface PasswordStrengthResult {\n  score: number\n  label: PasswordStrength\n  color: string\n  barColor: string\n  percent: number\n}\n\n/** Same scoring as React: length >= 6 / >= 10, mixed case, a digit, a symbol. */\nexport function computeStrength(pwd: string): PasswordStrengthResult {\n  if (!pwd) return { score: 0, label: 'weak', color: '', barColor: 'bg-transparent', percent: 0 }\n  let score = 0\n  if (pwd.length >= 6) score++\n  if (pwd.length >= 10) score++\n  if (/[A-Z]/.test(pwd) && /[a-z]/.test(pwd)) score++\n  if (/\\d/.test(pwd)) score++\n  if (/[^A-Za-z0-9]/.test(pwd)) score++\n  if (score <= 1) return { score, label: 'weak', color: 'text-destructive', barColor: 'bg-destructive', percent: 25 }\n  if (score <= 2) return { score, label: 'fair', color: 'text-warning', barColor: 'bg-warning', percent: 50 }\n  if (score <= 3) return { score, label: 'good', color: 'text-info', barColor: 'bg-info', percent: 75 }\n  return { score, label: 'strong', color: 'text-success', barColor: 'bg-success', percent: 100 }\n}\n\n/**\n * Angular port of the React PasswordInput. `<ui-password-input>` is React's outer\n * `flex w-full flex-col gap-2` column: the bordered control (`data-slot=\"password-input\"`,\n * which receives `class`) with the native input and the Eye / EyeOff toggle, then the\n * optional strength meter (role=status) or the minimum-length hint.\n *\n * Model is React's `value` / `defaultValue`, with `valueChange` (React `onChange`) emitting\n * the text. NG_VALUE_ACCESSOR is provided for formControl / ngModel.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-password-input',\n  standalone: true,\n  exportAs: 'uiPasswordInput',\n  providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => UiPasswordInputComponent), multi: true }],\n  host: {\n    '[attr.class]': '\"flex w-full flex-col gap-2\"',\n    // These belong to the native <input>.\n    '[attr.id]': 'null',\n    '[attr.placeholder]': 'null',\n    '[attr.aria-label]': 'null',\n    '[attr.aria-describedby]': 'null',\n    '[attr.aria-invalid]': 'null',\n  },\n  template: `\n    <div\n      data-uipkge=\"\"\n      data-slot=\"password-input\"\n      [attr.data-size]=\"size\"\n      [attr.data-variant]=\"variant\"\n      [class]=\"wrapperClasses\"\n    >\n      <input\n        #inputEl\n        [attr.id]=\"id ?? null\"\n        [value]=\"currentValue\"\n        [type]=\"passwordVisible() ? 'text' : 'password'\"\n        [disabled]=\"disabled\"\n        [readOnly]=\"readOnly\"\n        [attr.maxlength]=\"maxLength ?? null\"\n        [attr.placeholder]=\"placeholder\"\n        [attr.name]=\"name ?? null\"\n        [attr.autocomplete]=\"autoComplete\"\n        [attr.aria-label]=\"ariaLabel ?? null\"\n        [attr.aria-describedby]=\"ariaDescribedby ?? null\"\n        [attr.aria-invalid]=\"ariaInvalid ?? null\"\n        [required]=\"required\"\n        [class]=\"inputClass\"\n        (input)=\"handleInput($event)\"\n        (focus)=\"focusEvent.emit($event)\"\n        (blur)=\"onTouched(); blurEvent.emit($event)\"\n      />\n      @if (showToggle) {\n        <div [class]=\"toggleWrapClass\">\n          <button\n            type=\"button\"\n            [attr.aria-label]=\"passwordVisible() ? 'Hide password' : 'Show password'\"\n            [attr.aria-pressed]=\"passwordVisible()\"\n            [disabled]=\"disabled || readOnly\"\n            class=\"text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 shrink-0 rounded p-0.5 transition-colors focus-visible:ring-1 focus-visible:outline-none disabled:cursor-not-allowed\"\n            (mousedown)=\"$event.preventDefault()\"\n            (click)=\"toggleVisibility()\"\n          >\n            @if (passwordVisible()) {\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-eye-off size-4\"\n                aria-hidden=\"true\"\n              >\n                <path\n                  d=\"M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49\"\n                />\n                <path d=\"M14.084 14.158a3 3 0 0 1-4.242-4.242\" />\n                <path\n                  d=\"M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143\"\n                />\n                <path d=\"m2 2 20 20\" />\n              </svg>\n            } @else {\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-eye size-4\"\n                aria-hidden=\"true\"\n              >\n                <path\n                  d=\"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0\"\n                />\n                <circle cx=\"12\" cy=\"12\" r=\"3\" />\n              </svg>\n            }\n          </button>\n        </div>\n      }\n    </div>\n    @if (showStrength && currentValue) {\n      <div\n        class=\"flex flex-col gap-1.5\"\n        role=\"status\"\n        aria-live=\"polite\"\n        [attr.aria-label]=\"'Password strength: ' + strength.label\"\n      >\n        <div class=\"bg-muted h-1.5 w-full overflow-hidden rounded-full\" aria-hidden=\"true\">\n          <div [class]=\"barClass\" [style.width.%]=\"strength.percent\"></div>\n        </div>\n        <div class=\"flex items-center justify-between text-xs\">\n          <span [class]=\"labelClass\">{{ strength.label }}</span>\n          @if (minLength > 0) {\n            <span [class]=\"meetsMinLength ? 'text-success' : 'text-muted-foreground'\"\n              >{{ currentValue.length }} / {{ minLength }} chars</span\n            >\n          }\n        </div>\n      </div>\n    }\n    @if (minLength > 0 && !showStrength && currentValue) {\n      <p class=\"text-muted-foreground text-xs\">Minimum {{ minLength }} characters</p>\n    }\n  `,\n})\nexport class UiPasswordInputComponent implements ControlValueAccessor, AfterViewChecked {\n  // Zoneless-safe: form writes happen outside template events, so schedule a repaint.\n  private readonly cdr = inject(ChangeDetectorRef)\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  readonly passwordVisible = signal(false)\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 === undefined ? undefined : String(v ?? ''))\n  }\n  get value(): string {\n    return this.currentValue\n  }\n  @Input() defaultValue?: string\n  /** React `onChange`: emits the new text on every edit. */\n  @Output() valueChange = new EventEmitter<string>()\n  @Output('focus') focusEvent = new EventEmitter<FocusEvent>()\n  @Output('blur') blurEvent = new EventEmitter<FocusEvent>()\n\n  @Input() placeholder = 'Enter password'\n  @Input() size: PasswordInputSize = 'default'\n  @Input() variant: PasswordInputVariant = 'outlined'\n  @Input({ transform: booleanAttribute }) disabled = false\n  @Input({ transform: booleanAttribute }) readOnly = false\n  /** Lower-case HTML spelling of `readOnly`. */\n  @Input({ alias: 'readonly', transform: booleanAttribute }) set readonlyAlias(v: boolean) {\n    this.readOnly = v\n  }\n  @Input({ transform: booleanAttribute }) showStrength = false\n  @Input({ transform: booleanAttribute }) showToggle = true\n  @Input({ transform: numberAttribute }) minLength = 0\n  @Input() maxLength?: number\n  @Input() id?: string\n  @Input() name?: string\n  @Input() autoComplete = 'current-password'\n  @Input({ transform: booleanAttribute }) required = false\n  @Input('aria-label') ariaLabel?: string\n  @Input('aria-describedby') ariaDescribedby?: string\n  @Input('aria-invalid') ariaInvalid?: string | boolean\n  /** Wrapper className: lands on the bordered control, not the <input>. */\n  @Input('class') className?: string\n\n  private onChange: (v: string) => void = () => {}\n  onTouched: () => void = () => {}\n\n  get isControlled(): boolean {\n    return this._valueProp() !== undefined\n  }\n  get currentValue(): string {\n    return this._valueProp() ?? this._internal() ?? (this.defaultValue != null ? String(this.defaultValue) : '')\n  }\n  get strength(): PasswordStrengthResult {\n    return computeStrength(this.currentValue)\n  }\n  get meetsMinLength(): boolean {\n    return this.currentValue.length >= this.minLength\n  }\n\n  get wrapperClasses(): string {\n    return cn(\n      passwordInputVariants({ size: this.size, variant: this.variant }),\n      'focus-within:border-ring focus-within:ring-ring/50 focus-within:ring-[3px]',\n      this.disabled && 'pointer-events-none opacity-50 cursor-not-allowed bg-muted/30',\n      this.className,\n    )\n  }\n\n  get inputClass(): string {\n    const pad = this.size === 'sm' ? 'px-2.5' : this.size === 'lg' ? 'px-4' : 'px-3'\n    return cn('placeholder:text-muted-foreground w-full min-w-0 flex-1 bg-transparent outline-none', pad)\n  }\n\n  get toggleWrapClass(): string {\n    return cn('flex shrink-0 items-center', this.size === 'sm' ? 'pr-2' : this.size === 'lg' ? 'pr-3' : 'pr-2.5')\n  }\n\n  get barClass(): string {\n    return cn('h-full rounded-full transition-[width] duration-300', this.strength.barColor)\n  }\n\n  get labelClass(): string {\n    return cn('font-medium capitalize', this.strength.color)\n  }\n\n  toggleVisibility(): void {\n    if (this.disabled || this.readOnly) return\n    this.passwordVisible.update((v) => !v)\n    this.inputEl?.nativeElement.focus()\n  }\n\n  handleInput(event: Event): void {\n    const next = (event.target as HTMLInputElement).value\n    if (!this.isControlled) this._internal.set(next)\n    this.onChange(next)\n    this.valueChange.emit(next)\n  }\n\n  /** A controlled parent that did not take the edit wins: put its value back (React semantics). */\n  ngAfterViewChecked(): void {\n    const el = this.inputEl?.nativeElement\n    if (el && this.isControlled && el.value !== this.currentValue) el.value = this.currentValue\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",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/password-input/password-input.component.ts"
    },
    {
      "path": "packages/registry-angular/components/password-input/password-input.variants.ts",
      "content": "import type { VariantProps } from 'class-variance-authority'\nimport { cva } from 'class-variance-authority'\n\nexport const passwordInputVariants = cva(\n  'flex w-full items-center gap-1.5 overflow-hidden border transition-[color,box-shadow] outline-none rounded-md',\n  {\n    variants: {\n      size: {\n        sm: 'h-8 text-xs',\n        default: 'h-9 text-base md:text-sm',\n        lg: 'h-11 text-base',\n      },\n      variant: {\n        outlined: 'border-input bg-transparent shadow-xs',\n        filled: 'border-transparent bg-muted/50 shadow-none',\n        borderless: 'border-transparent bg-transparent shadow-none',\n      },\n    },\n    defaultVariants: {\n      size: 'default',\n      variant: 'outlined',\n    },\n  },\n)\n\nexport type PasswordInputVariants = VariantProps<typeof passwordInputVariants>\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/password-input/password-input.variants.ts"
    },
    {
      "path": "packages/registry-angular/components/password-input/index.ts",
      "content": "export {\n  UiPasswordInputComponent,\n  computeStrength,\n  type PasswordInputSize,\n  type PasswordInputVariant,\n  type PasswordStrength,\n  type PasswordStrengthResult,\n} from './password-input.component'\nexport { passwordInputVariants, type PasswordInputVariants } from './password-input.variants'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/password-input/index.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority",
    "@angular/forms"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Password input with show/hide toggle (Eye/EyeOff) and optional strength meter (weak/fair/good/strong with colored bar). Supports min length display, disabled, placeholder, and size variants.",
  "categories": [
    "control",
    "form"
  ]
}