{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "password",
  "title": "PasswordField",
  "description": "Password field for BuzzForm v2 with strength UI.",
  "registryDependencies": [
    "@buzzform/init",
    "input",
    "field",
    "@buzzform/copy"
  ],
  "files": [
    {
      "path": "registry/shadcn/fields/password.tsx",
      "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport type { PasswordField as PasswordFieldDef } from \"@buildnbuzz/form-react\";\nimport { useDataField } from \"@buildnbuzz/form-react\";\nimport { Input } from \"@/components/ui/input\";\nimport { Button } from \"@/components/ui/button\";\nimport { CopyButton } from \"../components/copy-button\";\nimport { IconPlaceholder } from \"@/components/icon-placeholder\";\nimport {\n  Field,\n  FieldContent,\n  FieldDescription,\n  FieldError,\n  FieldGroup,\n  FieldLabel,\n} from \"@/components/ui/field\";\nimport { cn } from \"@/lib/utils\";\n\ninterface PasswordUi {\n  /** Show a password strength indicator bar. */\n  strengthIndicator?: boolean;\n  /** Show a checklist of password requirements. */\n  showRequirements?: boolean;\n  /** Show a generate strong password button. */\n  allowGenerate?: boolean;\n  /** Show a copy button. */\n  copyable?: boolean;\n  autoFocus?: boolean;\n  className?: string;\n  width?: string | number;\n  asterisk?: boolean;\n  /** Text and label overrides. */\n  labels?: {\n    /** Labels for different strength levels. */\n    strength?: Partial<Record<StrengthResult[\"label\"], React.ReactNode>>;\n    /** Labels for individual requirements. */\n    requirements?: {\n      minLength?: React.ReactNode | ((min: number) => React.ReactNode);\n      maxLength?: React.ReactNode | ((max: number) => React.ReactNode);\n      uppercase?: React.ReactNode;\n      lowercase?: React.ReactNode;\n      number?: React.ReactNode;\n      special?: React.ReactNode;\n    };\n    /** A11y and tooltip strings (must be strings for HTML attributes). */\n    aria?: {\n      generate?: string;\n      showPassword?: string;\n      hidePassword?: string;\n      requirementsList?: string;\n    };\n  };\n}\n\ninterface StrengthResult {\n  score: 0 | 1 | 2 | 3 | 4;\n  label: \"none\" | \"weak\" | \"fair\" | \"strong\" | \"excellent\";\n  allMet: boolean;\n  checks: {\n    minLength: boolean;\n    maxLength: boolean;\n    hasUppercase: boolean;\n    hasLowercase: boolean;\n    hasNumber: boolean;\n    hasSpecial: boolean;\n  };\n}\n\nconst STRENGTH_LABELS: Record<StrengthResult[\"label\"], string> = {\n  none: \"\",\n  weak: \"Weak\",\n  fair: \"Fair\",\n  strong: \"Strong\",\n  excellent: \"Excellent\",\n};\n\nfunction calculateStrength(\n  value: string,\n  field: PasswordFieldDef,\n): StrengthResult {\n  const minLength = field.minLength ?? 8;\n  const criteria = field.criteria ?? {};\n\n  const checks = {\n    minLength: value.length >= minLength,\n    maxLength: field.maxLength ? value.length <= field.maxLength : true,\n    hasUppercase: /[A-Z]/.test(value),\n    hasLowercase: /[a-z]/.test(value),\n    hasNumber: /[0-9]/.test(value),\n    hasSpecial: /[^A-Za-z0-9]/.test(value),\n  };\n\n  const required = [\n    { met: checks.minLength },\n    ...(field.maxLength ? [{ met: checks.maxLength }] : []),\n    ...(criteria.requireUppercase ? [{ met: checks.hasUppercase }] : []),\n    ...(criteria.requireLowercase ? [{ met: checks.hasLowercase }] : []),\n    ...(criteria.requireNumber ? [{ met: checks.hasNumber }] : []),\n    ...(criteria.requireSpecial ? [{ met: checks.hasSpecial }] : []),\n  ];\n\n  const allMet = required.every((r) => r.met);\n\n  if (!value) return { score: 0, label: \"none\", allMet: false, checks };\n\n  const variety = [\n    checks.hasUppercase,\n    checks.hasLowercase,\n    checks.hasNumber,\n    checks.hasSpecial,\n  ].filter(Boolean).length;\n\n  let score: number;\n  if (value.length < 4) score = 0;\n  else if (value.length < minLength) score = 1;\n  else {\n    score = Math.min(variety, 4);\n    if (value.length >= 16) score = Math.min(score + 1, 4);\n  }\n\n  if (!allMet) score = Math.min(score, 2);\n\n  const labels: Record<number, StrengthResult[\"label\"]> = {\n    0: \"none\", 1: \"weak\", 2: \"fair\", 3: \"strong\", 4: \"excellent\",\n  };\n\n  return {\n    score: score as StrengthResult[\"score\"],\n    label: labels[score] ?? \"none\",\n    allMet,\n    checks,\n  };\n}\n\nfunction generatePassword(field: PasswordFieldDef): string {\n  const criteria = field.criteria ?? {};\n  const minLength = Math.max(field.minLength ?? 16, 12);\n  const upper = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\";\n  const lower = \"abcdefghijklmnopqrstuvwxyz\";\n  const numbers = \"0123456789\";\n  const special = \"!@#$%^&*()_+-=[]{}|;:,.<>?\";\n\n  let pool = lower + upper + numbers;\n  let password = \"\";\n\n  if (criteria.requireUppercase) { pool += upper; password += upper[Math.floor(Math.random() * upper.length)]; }\n  if (criteria.requireLowercase) { pool += lower; password += lower[Math.floor(Math.random() * lower.length)]; }\n  if (criteria.requireNumber) { pool += numbers; password += numbers[Math.floor(Math.random() * numbers.length)]; }\n  if (criteria.requireSpecial) { pool += special; password += special[Math.floor(Math.random() * special.length)]; }\n\n  const remaining = Math.max(0, minLength - password.length);\n  for (let i = 0; i < remaining; i++) {\n    password += pool[Math.floor(Math.random() * pool.length)];\n  }\n\n  return password.split(\"\").sort(() => 0.5 - Math.random()).join(\"\");\n}\n\nfunction getStrengthBarColor(score: number, allMet: boolean): string {\n  if (allMet && score >= 3) return \"bg-green-500\";\n  if (score >= 3) return \"bg-emerald-500\";\n  if (score === 2) return \"bg-orange-500\";\n  return \"bg-red-500\";\n}\n\nfunction getStrengthTextColor(score: number, allMet: boolean): string {\n  if (allMet && score >= 3) return \"text-green-600 dark:text-green-400\";\n  if (score >= 3) return \"text-emerald-600 dark:text-emerald-400\";\n  if (score === 2) return \"text-orange-600 dark:text-orange-400\";\n  return \"text-red-600 dark:text-red-400\";\n}\n\nexport function PasswordField() {\n  const {\n    fieldApi,\n    field,\n    isDisabled,\n    isReadOnly,\n    isRequired,\n    label,\n    placeholder,\n    description,\n    errors,\n    isInvalid,\n    descriptionId,\n    errorId,\n    ariaDescribedBy,\n    handleChange,\n    handleBlur,\n  } = useDataField<PasswordFieldDef>();\n\n  const [visible, setVisible] = useState(false);\n  const value = (fieldApi.state.value as string) ?? \"\";\n  const ui = field.ui as PasswordUi | undefined;\n  const labels = ui?.labels;\n  const criteria = field.criteria;\n\n  const width = ui?.width;\n  const widthStyle = width\n    ? {\n        width: typeof width === \"number\" ? `${width}px` : width,\n        flex: \"0 1 auto\",\n      }\n    : undefined;\n\n  const strength =\n    ui?.strengthIndicator || ui?.showRequirements\n      ? calculateStrength(value, field)\n      : null;\n\n  const resolveRequirementLabel = <\n    K extends keyof NonNullable<\n      NonNullable<PasswordUi[\"labels\"]>[\"requirements\"]\n    >,\n  >(\n    key: K,\n    defaultLabel: React.ReactNode,\n    arg?: number,\n  ): React.ReactNode => {\n    const override = labels?.requirements?.[key];\n    if (typeof override === \"function\" && arg !== undefined) {\n      // We know arg is a number and the only functions here take a number\n      return (override as (v: number) => React.ReactNode)(arg);\n    }\n    return (override as React.ReactNode) ?? defaultLabel;\n  };\n\n  const requirements = ui?.showRequirements\n    ? [\n        {\n          key: \"minLength\",\n          label: resolveRequirementLabel(\n            \"minLength\",\n            `At least ${field.minLength ?? 8} characters`,\n            field.minLength ?? 8,\n          ),\n          met: strength?.checks.minLength ?? false,\n        },\n        ...(field.maxLength\n          ? [\n              {\n                key: \"maxLength\",\n                label: resolveRequirementLabel(\n                  \"maxLength\",\n                  `At most ${field.maxLength} characters`,\n                  field.maxLength,\n                ),\n                met: strength?.checks.maxLength ?? false,\n              },\n            ]\n          : []),\n        ...(criteria?.requireUppercase\n          ? [\n              {\n                key: \"uppercase\",\n                label: resolveRequirementLabel(\"uppercase\", \"One uppercase letter\"),\n                met: strength?.checks.hasUppercase ?? false,\n              },\n            ]\n          : []),\n        ...(criteria?.requireLowercase\n          ? [\n              {\n                key: \"lowercase\",\n                label: resolveRequirementLabel(\"lowercase\", \"One lowercase letter\"),\n                met: strength?.checks.hasLowercase ?? false,\n              },\n            ]\n          : []),\n        ...(criteria?.requireNumber\n          ? [\n              {\n                key: \"number\",\n                label: resolveRequirementLabel(\"number\", \"One number\"),\n                met: strength?.checks.hasNumber ?? false,\n              },\n            ]\n          : []),\n        ...(criteria?.requireSpecial\n          ? [\n              {\n                key: \"special\",\n                label: resolveRequirementLabel(\"special\", \"One special character\"),\n                met: strength?.checks.hasSpecial ?? false,\n              },\n            ]\n          : []),\n      ]\n    : [];\n\n  // pr class based on visible action buttons\n  const inputPrClass = cn(\n    \"pr-9\",\n    ui?.allowGenerate && \"pr-16\",\n    ui?.allowGenerate && ui?.copyable && \"pr-24\",\n  );\n\n  return (\n    <FieldGroup\n      data-field={fieldApi.name}\n      className={ui?.className}\n      style={widthStyle}\n    >\n      <Field data-invalid={isInvalid} data-disabled={isDisabled}>\n        {label && (\n          <FieldLabel htmlFor={fieldApi.name} className=\"gap-1 items-baseline\">\n            {label}\n            {isRequired && ui?.asterisk !== false ? (\n              <span className=\"text-destructive\">*</span>\n            ) : null}\n          </FieldLabel>\n        )}\n\n        <FieldContent>\n          <div className=\"space-y-2\">\n            <div className=\"relative flex items-center\">\n              <Input\n                id={fieldApi.name}\n                name={fieldApi.name}\n                type={visible ? \"text\" : \"password\"}\n                autoComplete={field.autoComplete ?? \"current-password\"}\n                autoFocus={ui?.autoFocus}\n                value={value}\n                onChange={(e) => handleChange(e.target.value)}\n                onBlur={handleBlur}\n                placeholder={placeholder}\n                disabled={isDisabled}\n                readOnly={isReadOnly}\n                aria-invalid={isInvalid}\n                aria-describedby={ariaDescribedBy}\n                minLength={field.minLength}\n                maxLength={field.maxLength}\n                required={isRequired}\n                className={inputPrClass}\n              />\n              <div className=\"absolute right-0 top-0 h-full flex items-center gap-0.5 pr-1\">\n                {ui?.copyable && value && (\n                  <CopyButton\n                    value={value}\n                    disabled={isDisabled}\n                    className=\"h-7 w-7 text-muted-foreground hover:text-foreground\"\n                  />\n                )}\n                {ui?.allowGenerate && (\n                  <Button\n                    type=\"button\"\n                    variant=\"ghost\"\n                    size=\"icon\"\n                    onClick={() => {\n                      handleChange(generatePassword(field));\n                    }}\n                    disabled={isDisabled || isReadOnly}\n                    aria-label={labels?.aria?.generate ?? \"Generate password\"}\n                    tabIndex={-1}\n                  >\n                    <IconPlaceholder\n                      hugeicons=\"MagicWand01Icon\"\n                      lucide=\"Sparkles\"\n                      tabler=\"IconWand\"\n                      phosphor=\"MagicWand\"\n                      remixicon=\"RiMagicLine\"\n                      className=\"size-3.5\"\n                    />\n                  </Button>\n                )}\n                <Button\n                  type=\"button\"\n                  variant=\"ghost\"\n                  size=\"icon\"\n                  className=\"text-muted-foreground hover:text-foreground\"\n                  onClick={() => setVisible((v) => !v)}\n                  disabled={isDisabled}\n                  aria-label={\n                    visible\n                      ? (labels?.aria?.hidePassword ?? \"Hide password\")\n                      : (labels?.aria?.showPassword ?? \"Show password\")\n                  }\n                  tabIndex={-1}\n                >\n                  {visible ? (\n                    <IconPlaceholder\n                      hugeicons=\"ViewOffIcon\"\n                      lucide=\"EyeOff\"\n                      tabler=\"IconEyeOff\"\n                      phosphor=\"EyeSlash\"\n                      remixicon=\"RiEyeOffLine\"\n                      className=\"size-4\"\n                    />\n                  ) : (\n                    <IconPlaceholder\n                      hugeicons=\"ViewIcon\"\n                      lucide=\"Eye\"\n                      tabler=\"IconEye\"\n                      phosphor=\"Eye\"\n                      remixicon=\"RiEyeLine\"\n                      className=\"size-4\"\n                    />\n                  )}\n                </Button>\n              </div>\n            </div>\n\n            {ui?.strengthIndicator && value && strength && (\n              <div className=\"flex items-center gap-2\">\n                <div className=\"flex gap-1 h-1 flex-1\">\n                  {[0, 1, 2, 3].map((i) => (\n                    <div\n                      key={i}\n                      className={cn(\n                        \"h-full flex-1 rounded-full transition-colors\",\n                        i < strength.score\n                          ? getStrengthBarColor(strength.score, strength.allMet)\n                          : \"bg-muted\",\n                      )}\n                    />\n                  ))}\n                </div>\n                {strength.label !== \"none\" && (\n                  <span\n                    className={cn(\n                      \"text-xs font-medium transition-colors\",\n                      getStrengthTextColor(strength.score, strength.allMet),\n                    )}\n                  >\n                    {labels?.strength?.[strength.label] ??\n                      STRENGTH_LABELS[strength.label]}\n                  </span>\n                )}\n              </div>\n            )}\n\n            {requirements.length > 0 && (\n              <div\n                id={`${fieldApi.name}-requirements`}\n                className=\"text-xs space-y-1\"\n                role=\"list\"\n                aria-label={\n                  labels?.aria?.requirementsList ?? \"Password requirements\"\n                }\n              >\n                {requirements.map((req) => (\n                  <div\n                    key={req.key}\n                    role=\"listitem\"\n                    className={cn(\n                      \"flex items-center gap-1.5 transition-colors\",\n                      value\n                        ? req.met\n                          ? \"text-green-600 dark:text-green-400\"\n                          : \"text-muted-foreground\"\n                        : \"text-muted-foreground\",\n                    )}\n                  >\n                    {value ? (\n                      req.met ? (\n                        <IconPlaceholder\n                          hugeicons=\"Tick02Icon\"\n                          lucide=\"Check\"\n                          tabler=\"IconCheck\"\n                          phosphor=\"Check\"\n                          remixicon=\"RiCheckLine\"\n                          className=\"size-3 shrink-0 text-green-500\"\n                        />\n                      ) : (\n                        <IconPlaceholder\n                          hugeicons=\"Cancel01Icon\"\n                          lucide=\"X\"\n                          tabler=\"IconX\"\n                          phosphor=\"X\"\n                          remixicon=\"RiCloseLine\"\n                          className=\"size-3 shrink-0\"\n                        />\n                      )\n                    ) : (\n                      <span className=\"size-3 rounded-full border border-muted-foreground/50 shrink-0\" />\n                    )}\n                    <span>{req.label}</span>\n                  </div>\n                ))}\n              </div>\n            )}\n          </div>\n        </FieldContent>\n\n        {description && !isInvalid && (\n          <FieldDescription id={descriptionId}>{description}</FieldDescription>\n        )}\n\n        {isInvalid && <FieldError id={errorId} errors={errors} />}\n      </Field>\n    </FieldGroup>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/buzzform/fields/password.tsx"
    }
  ],
  "type": "registry:ui"
}