{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "password-input",
  "title": "Password Input",
  "type": "registry:ui",
  "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"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/password-input/password-input.ts",
      "content": "import { LitElement, css, html, nothing } from 'lit'\nimport { live } from 'lit/directives/live.js'\nimport { styleMap } from 'lit/directives/style-map.js'\nimport { Eye, EyeOff } from 'lucide'\nimport { cn } from '../../lib/utils'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport { icon } from '../../lib/icon'\nimport { passwordInputVariants, type PasswordInputVariants } from './password-input.variants'\n\ninterface StrengthResult {\n  score: number\n  label: 'weak' | 'fair' | 'good' | 'strong'\n  color: string\n  barColor: string\n  percent: number\n}\n\nfunction computeStrength(pwd: string): StrengthResult {\n  if (!pwd) return { score: 0, label: 'weak', color: '', barColor: 'bg-transparent', percent: 0 }\n\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\n  if (score <= 1) {\n    return { score, label: 'weak', color: 'text-destructive', barColor: 'bg-destructive', percent: 25 }\n  }\n  if (score <= 2) {\n    return {\n      score,\n      label: 'fair',\n      color: 'text-warning',\n      barColor: 'bg-warning',\n      percent: 50,\n    }\n  }\n  if (score <= 3) {\n    return {\n      score,\n      label: 'good',\n      color: 'text-info',\n      barColor: 'bg-info',\n      percent: 75,\n    }\n  }\n  return {\n    score,\n    label: 'strong',\n    color: 'text-success',\n    barColor: 'bg-success',\n    percent: 100,\n  }\n}\n\nconst trueUnlessFalse = {\n  fromAttribute: (v: string | null) => v !== 'false',\n}\n\n/**\n * <uip-password-input> — the registry PasswordInput as a web component.\n *\n * Form-associated: submits `name=value` with its <form>, supports reset and validity.\n */\nexport class UipPasswordInput extends LitElement {\n  static formAssociated = true\n  static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true }\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  static properties = {\n    value: { reflect: true },\n    defaultValue: { attribute: 'default-value' },\n    placeholder: {},\n    size: { reflect: true },\n    variant: { reflect: true },\n    disabled: { type: Boolean, reflect: true },\n    readOnly: { type: Boolean, attribute: 'readonly', reflect: true },\n    required: { type: Boolean, reflect: true },\n    showStrength: { type: Boolean, attribute: 'show-strength' },\n    showToggle: { converter: trueUnlessFalse, attribute: 'show-toggle' },\n    minLength: { type: Number, attribute: 'minlength' },\n    maxLength: { type: Number, attribute: 'maxlength' },\n    name: { reflect: true },\n    autocomplete: {},\n    accessibleLabel: { attribute: 'aria-label' },\n    passwordVisible: { state: true },\n    focused: { state: true },\n  }\n\n  value = ''\n  defaultValue?: string\n  placeholder = 'Enter password'\n  size: NonNullable<PasswordInputVariants['size']> = 'default'\n  variant: NonNullable<PasswordInputVariants['variant']> = 'outlined'\n  disabled = false\n  readOnly = false\n  required = false\n  showStrength = false\n  showToggle = true\n  minLength = 0\n  maxLength?: number\n  name?: string\n  autocomplete = 'current-password'\n  accessibleLabel?: string\n\n  private passwordVisible = false\n  private focused = false\n  private initialValue = ''\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', 'password-input')\n    if (this.defaultValue !== undefined && this.getAttribute('value') === null) {\n      this.value = this.defaultValue\n    }\n    this.initialValue = this.value\n  }\n\n  protected willUpdate(changed: Map<string, unknown>) {\n    if (changed.has('value')) {\n      this.internals.setFormValue(this.value ?? '')\n    }\n  }\n\n  protected updated() {\n    this.syncValidity()\n  }\n\n  private get input() {\n    return this.renderRoot?.querySelector<HTMLInputElement>('input')\n  }\n\n  private syncValidity() {\n    const i = this.input\n    if (!i) return\n    if (i.validity.valid) this.internals.setValidity({})\n    else this.internals.setValidity(i.validity, i.validationMessage, i)\n  }\n\n  formResetCallback() {\n    this.value = this.initialValue\n  }\n\n  formDisabledCallback(disabled: boolean) {\n    this.disabled = disabled\n  }\n\n  private onInput(e: Event) {\n    this.value = (e.target as HTMLInputElement).value\n    this.dispatchEvent(new CustomEvent('value-change', { detail: { value: this.value }, bubbles: true, composed: true }))\n  }\n\n  private onChange() {\n    this.dispatchEvent(new Event('change', { bubbles: true, composed: true }))\n  }\n\n  private onKeypress(e: KeyboardEvent) {\n    if (e.key !== 'Enter' || e.defaultPrevented || this.disabled) return\n    const form = this.internals.form\n    if (!form) return\n    e.preventDefault()\n    form.requestSubmit()\n  }\n\n  private togglePassword() {\n    if (this.disabled || this.readOnly) return\n    this.passwordVisible = !this.passwordVisible\n    this.input?.focus()\n  }\n\n  render() {\n    const currentValue = this.value ?? ''\n    const computedType = this.passwordVisible ? 'text' : 'password'\n    const strength = computeStrength(currentValue)\n    const meetsMinLength = currentValue.length >= this.minLength\n\n    const wrapperClasses = 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    )\n\n    const inputPadding = this.size === 'sm' ? 'px-2.5' : this.size === 'lg' ? 'px-4' : 'px-3'\n    const togglePadding = this.size === 'sm' ? 'pr-2' : this.size === 'lg' ? 'pr-3' : 'pr-2.5'\n\n    return html`\n      <div part=\"base\" class=\"flex w-full flex-col gap-2\">\n        <div\n          part=\"control\"\n          class=${wrapperClasses}\n          data-slot=\"password-input\"\n          data-size=${this.size}\n          data-variant=${this.variant}\n        >\n          <input\n            part=\"input\"\n            .value=${live(currentValue)}\n            type=${computedType}\n            name=${this.name ?? nothing}\n            placeholder=${this.placeholder}\n            ?disabled=${this.disabled}\n            ?readonly=${this.readOnly}\n            ?required=${this.required}\n            minlength=${this.minLength > 0 ? this.minLength : nothing}\n            maxlength=${this.maxLength ?? nothing}\n            autocomplete=${this.autocomplete}\n            aria-label=${this.accessibleLabel ?? nothing}\n            class=${cn(\n              'placeholder:text-muted-foreground w-full min-w-0 flex-1 bg-transparent outline-none text-foreground',\n              inputPadding,\n            )}\n            @input=${this.onInput}\n            @change=${this.onChange}\n            @keypress=${this.onKeypress}\n            @focus=${() => (this.focused = true)}\n            @blur=${() => (this.focused = false)}\n          />\n\n          ${this.showToggle\n            ? html`\n                <div class=${cn('flex shrink-0 items-center', togglePadding)}>\n                  <button\n                    type=\"button\"\n                    aria-label=${this.passwordVisible ? 'Hide password' : 'Show password'}\n                    aria-pressed=${this.passwordVisible ? 'true' : 'false'}\n                    ?disabled=${this.disabled || this.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=${(e: MouseEvent) => e.preventDefault()}\n                    @click=${this.togglePassword}\n                  >\n                    ${this.passwordVisible ? icon(EyeOff, 'eye-off', 'size-4') : icon(Eye, 'eye', 'size-4')}\n                  </button>\n                </div>\n              `\n            : nothing}\n        </div>\n\n        ${this.showStrength && currentValue\n          ? html`\n              <div\n                class=\"flex flex-col gap-1.5\"\n                role=\"status\"\n                aria-live=\"polite\"\n                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\n                    class=${cn('h-full rounded-full transition-[width] duration-300', strength.barColor)}\n                    style=${styleMap({ width: `${strength.percent}%` })}\n                  ></div>\n                </div>\n                <div class=\"flex items-center justify-between text-xs\">\n                  <span class=${cn('font-medium capitalize', strength.color)}>${strength.label}</span>\n                  ${this.minLength > 0\n                    ? html`\n                        <span class=${meetsMinLength ? 'text-success' : 'text-muted-foreground'}>\n                          ${currentValue.length} / ${this.minLength} chars\n                        </span>\n                      `\n                    : nothing}\n                </div>\n              </div>\n            `\n          : nothing}\n        ${this.minLength > 0 && !this.showStrength && currentValue\n          ? html`<p class=\"text-muted-foreground text-xs\">Minimum ${this.minLength} characters</p>`\n          : nothing}\n      </div>\n    `\n  }\n}\n\ncustomElements.get('uip-password-input') || customElements.define('uip-password-input', UipPasswordInput)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-password-input': UipPasswordInput\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/password-input/password-input.ts"
    },
    {
      "path": "packages/elements-lit/src/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:file",
      "target": "src/uip/components/password-input/password-input.variants.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority",
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
