{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "password-rules",
  "title": "Password Rules",
  "author": "@fma-ds/ds",
  "description": "The rules a new password must follow, each marked pending, met or unmet as the user types — by icon and a screen-reader state label (`stateLabels`), not color alone.\n\n- **`rules`**: takes `PasswordRule` objects (`id`, `label`, `test(value, confirmation)`).\n- **`passwordRules`**: builds the usual ones — `minLength(n)`, `maxLength(n)`, `uppercase()`, `lowercase()`, `number()`, `special()`, `match()` against `confirmation` — each taking a `label` to translate it. Defaults to 8+ characters, upper, lower and a number.\n- **Neutral start**: rules stay neutral until there's something to judge.\n- **Validation**: the pure helpers `evaluatePasswordRules` and `isPasswordValid` share the same rules for form validation.\n\nGive it an `id` and point the input's `aria-describedby` at it.",
  "dependencies": [
    "cn@^0.5.0",
    "lucide-react@^1.61.0"
  ],
  "files": [
    {
      "path": "src/core/password-rules/password-rules.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"cn\";\nimport { CheckIcon, CircleIcon, XIcon } from \"lucide-react\";\nimport type * as React from \"react\";\n\ntype PasswordRule = {\n  id: string;\n  label: string;\n  /** `confirmation` is the \"repeat password\" value, for rules like `match`. */\n  test: (value: string, confirmation: string) => boolean;\n  /** Stays neutral until the confirmation is typed (instead of until the password is). */\n  dependsOnConfirmation?: boolean;\n};\n\ntype PasswordRuleState = \"neutral\" | \"met\" | \"unmet\";\n\n/** Factories for the usual rules; pass `label` to translate or reword them. */\nconst passwordRules = {\n  minLength: (min: number, label = `At least ${min} characters`): PasswordRule => ({\n    id: \"min-length\",\n    label,\n    test: (value) => [...value].length >= min,\n  }),\n  maxLength: (max: number, label = `At most ${max} characters`): PasswordRule => ({\n    id: \"max-length\",\n    label,\n    test: (value) => [...value].length <= max,\n  }),\n  uppercase: (label = \"An uppercase letter\"): PasswordRule => ({\n    id: \"uppercase\",\n    label,\n    test: (value) => /\\p{Lu}/u.test(value),\n  }),\n  lowercase: (label = \"A lowercase letter\"): PasswordRule => ({\n    id: \"lowercase\",\n    label,\n    test: (value) => /\\p{Ll}/u.test(value),\n  }),\n  number: (label = \"A number\"): PasswordRule => ({\n    id: \"number\",\n    label,\n    test: (value) => /\\p{N}/u.test(value),\n  }),\n  special: (label = \"A special character\"): PasswordRule => ({\n    id: \"special\",\n    label,\n    test: (value) => /[^\\p{L}\\p{N}\\s]/u.test(value),\n  }),\n  match: (label = \"Matches the confirmation\"): PasswordRule => ({\n    id: \"match\",\n    label,\n    test: (value, confirmation) => value.length > 0 && value === confirmation,\n    dependsOnConfirmation: true,\n  }),\n};\n\nconst defaultPasswordRules: PasswordRule[] = [\n  passwordRules.minLength(8),\n  passwordRules.uppercase(),\n  passwordRules.lowercase(),\n  passwordRules.number(),\n];\n\n/**\n * Pure check of every rule. A rule is `neutral` while there's nothing to judge\n * yet (empty password, or empty confirmation for confirmation rules).\n */\nfunction evaluatePasswordRules(\n  value: string,\n  rules: PasswordRule[] = defaultPasswordRules,\n  confirmation = \"\",\n): { rule: PasswordRule; state: PasswordRuleState }[] {\n  return rules.map((rule) => {\n    const pending = rule.dependsOnConfirmation ? !confirmation : !value;\n    if (pending) return { rule, state: \"neutral\" };\n    return { rule, state: rule.test(value, confirmation) ? \"met\" : \"unmet\" };\n  });\n}\n\n/** Whether the password passes every rule. */\nfunction isPasswordValid(\n  value: string,\n  rules: PasswordRule[] = defaultPasswordRules,\n  confirmation = \"\",\n): boolean {\n  return rules.every((rule) => rule.test(value, confirmation));\n}\n\nconst ICONS = { neutral: CircleIcon, met: CheckIcon, unmet: XIcon } as const;\n\ntype PasswordRulesProps = Omit<React.ComponentProps<\"ul\">, \"children\"> & {\n  value: string;\n  confirmation?: string;\n  rules?: PasswordRule[];\n  /** Read after each rule by screen readers, so the state isn't conveyed by color alone. */\n  stateLabels?: Record<PasswordRuleState, string>;\n};\n\n/**\n * The password rules, each marked pending, met or unmet as the user types.\n * Give it an `id` and point the password input's `aria-describedby` at it.\n */\nfunction PasswordRules({\n  value,\n  confirmation = \"\",\n  rules = defaultPasswordRules,\n  stateLabels = { neutral: \"pending\", met: \"met\", unmet: \"not met\" },\n  className,\n  ...props\n}: PasswordRulesProps) {\n  return (\n    <ul\n      data-slot=\"password-rules\"\n      className={cn(\"flex flex-col gap-1.5 text-sm\", className)}\n      {...props}\n    >\n      {evaluatePasswordRules(value, rules, confirmation).map(({ rule, state }) => {\n        const Icon = ICONS[state];\n        return (\n          <li\n            key={rule.id}\n            data-slot=\"password-rule\"\n            data-state={state}\n            className={cn(\n              \"flex items-center gap-2 text-muted-foreground transition-colors data-[state=met]:text-foreground\",\n              \"[&>svg]:size-4 [&>svg]:shrink-0 data-[state=met]:[&>svg]:text-success data-[state=neutral]:[&>svg]:size-2.5 data-[state=neutral]:[&>svg]:mx-[3px] data-[state=unmet]:[&>svg]:text-destructive\",\n            )}\n          >\n            <Icon aria-hidden=\"true\" />\n            <span>\n              {rule.label}\n              <span className=\"sr-only\">: {stateLabels[state]}</span>\n            </span>\n          </li>\n        );\n      })}\n    </ul>\n  );\n}\n\nexport type { PasswordRule, PasswordRuleState, PasswordRulesProps };\nexport {\n  defaultPasswordRules,\n  evaluatePasswordRules,\n  isPasswordValid,\n  PasswordRules,\n  passwordRules,\n};\n",
      "type": "registry:component",
      "target": "components/fma-ds/password-rules.tsx"
    }
  ],
  "meta": {
    "status": "stable",
    "source": "original",
    "license": "MIT",
    "tags": [
      "form",
      "password",
      "validation"
    ],
    "contentHash": "eec8b40a9a92"
  },
  "categories": [
    "forms"
  ],
  "type": "registry:ui"
}