{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "openlmis-password-requirements",
  "title": "Password Requirements",
  "description": "A live checklist of password rules with pure validation helpers.",
  "dependencies": [
    "lucide-react@^1",
    "zod@^4",
    "@base-ui/react@^1"
  ],
  "registryDependencies": [
    "input",
    "label",
    "utils"
  ],
  "files": [
    {
      "path": "registry/components/openlmis/password-requirements/password-requirements.tsx",
      "content": "import { CheckIcon, CircleIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nimport {\n  PASSWORD_RULE_LABELS,\n  type PasswordOwner,\n  passwordChecks,\n  passwordRules,\n} from \"@/components/openlmis/password-requirements/password-rules\"\n\ntype PasswordRequirementsProps = {\n  /** Point the password input's `aria-describedby` here, so the rules are read with it. */\n  id?: string\n  password: string\n  /** Adds the rule against the owner's username and names. */\n  owner?: PasswordOwner\n}\n\n/** The password rules as a checklist that ticks off each one as the password meets it. */\nexport function PasswordRequirements({\n  id,\n  password,\n  owner,\n}: PasswordRequirementsProps) {\n  const checks = passwordChecks(password, owner)\n\n  return (\n    <ul\n      aria-label=\"Password Requirements\"\n      className=\"grid gap-1 text-sm\"\n      id={id}\n    >\n      {passwordRules(owner).map((rule) => {\n        const met = checks[rule]\n        const Icon = met ? CheckIcon : CircleIcon\n        return (\n          <li\n            className={cn(\n              \"flex items-center gap-2\",\n              met ? \"text-success\" : \"text-muted-foreground\"\n            )}\n            key={rule}\n          >\n            <Icon\n              aria-hidden=\"true\"\n              className={met ? \"size-4 shrink-0\" : \"mx-0.5 size-3 shrink-0\"}\n            />\n            <span>{PASSWORD_RULE_LABELS[rule]}</span>\n            <span className=\"sr-only\">{met ? \"(met)\" : \"(not met yet)\"}</span>\n          </li>\n        )\n      })}\n    </ul>\n  )\n}",
      "type": "registry:component",
      "target": "components/openlmis/password-requirements/password-requirements.tsx"
    },
    {
      "path": "registry/components/openlmis/password-requirements/password-rules.ts",
      "content": "/** Whose password it is; the OpenLMIS auth service refuses one that contains any of these. */\nexport type PasswordOwner = {\n  username: string\n  firstName?: string | null\n  lastName?: string | null\n}\n\n/** The fixed rules the auth service checks, in the order it reports them; strength is left to the server. */\nconst PASSWORD_RULES = [\"length\", \"characters\", \"number\", \"names\"] as const\n\nexport type PasswordRule = (typeof PASSWORD_RULES)[number]\n\nexport const PASSWORD_RULE_LABELS: Record<PasswordRule, string> = {\n  length: \"8 to 72 characters\",\n  characters: \"Only letters A to Z and numbers\",\n  number: \"At least 1 number\",\n  names: \"Does not contain the username, first name or last name\",\n}\n\nconst PASSWORD_RULE_ERRORS: Record<PasswordRule, string> = {\n  length: \"Use 8 to 72 characters.\",\n  characters:\n    \"Use only the letters A to Z and numbers, with no spaces or symbols.\",\n  number: \"Include at least 1 number.\",\n  names: \"Leave out the username, first name and last name.\",\n}\n\n/** The rules that apply; the names rule only when the owner is known. */\nexport function passwordRules(owner?: PasswordOwner): readonly PasswordRule[] {\n  return owner\n    ? PASSWORD_RULES\n    : PASSWORD_RULES.filter((rule) => rule !== \"names\")\n}\n\nexport function passwordChecks(\n  password: string,\n  owner?: PasswordOwner\n): Record<PasswordRule, boolean> {\n  const lower = password.toLowerCase()\n  const names = [owner?.username, owner?.firstName, owner?.lastName]\n    .map((name) => name?.trim().toLowerCase())\n    .filter((name): name is string => Boolean(name))\n  return {\n    length: password.length >= 8 && password.length <= 72,\n    characters: /^[a-zA-Z0-9]+$/.test(password),\n    number: /\\d/.test(password),\n    names: password !== \"\" && names.every((name) => !lower.includes(name)),\n  }\n}\n\n/** What is wrong with the password: missing, or the first rule it misses. */\nexport function passwordIssue(\n  password: string,\n  owner?: PasswordOwner\n): string | undefined {\n  if (password === \"\") return \"Enter a password.\"\n  const checks = passwordChecks(password, owner)\n  const unmet = passwordRules(owner).find((rule) => !checks[rule])\n  return unmet && PASSWORD_RULE_ERRORS[unmet]\n}",
      "type": "registry:lib",
      "target": "components/openlmis/password-requirements/password-rules.ts"
    },
    {
      "path": "registry/components/openlmis/password-requirements/password-schema.ts",
      "content": "import { z } from \"zod\"\n\nimport { type PasswordOwner, passwordIssue } from \"@/components/openlmis/password-requirements/password-rules\"\n\n/** The new password, typed twice; with an owner, it may not contain their username or names. */\nexport function newPasswordSchema(owner?: PasswordOwner) {\n  return z\n    .object({ password: z.string(), confirm: z.string() })\n    .superRefine(({ password, confirm }, context) => {\n      const issue = passwordIssue(password, owner)\n      if (issue)\n        context.addIssue({ code: \"custom\", path: [\"password\"], message: issue })\n      if (confirm === \"\") {\n        context.addIssue({\n          code: \"custom\",\n          path: [\"confirm\"],\n          message: \"Type the new password again.\",\n        })\n      } else if (confirm !== password) {\n        context.addIssue({\n          code: \"custom\",\n          path: [\"confirm\"],\n          message: \"The passwords do not match.\",\n        })\n      }\n    })\n}",
      "type": "registry:lib",
      "target": "components/openlmis/password-requirements/password-schema.ts"
    }
  ],
  "cssVars": {
    "light": {
      "destructive": "oklch(0.577 0.245 27.325)",
      "destructive-foreground": "oklch(0.985 0 0)",
      "success": "oklch(0.596 0.145 163.225)",
      "success-foreground": "oklch(0.985 0 0)",
      "warning": "oklch(0.666 0.179 58.318)",
      "warning-foreground": "oklch(0.985 0 0)",
      "info": "oklch(0.55 0.13 237)",
      "info-foreground": "oklch(0.985 0 0)"
    },
    "dark": {
      "destructive": "oklch(0.704 0.191 22.216)",
      "destructive-foreground": "oklch(0.21 0.04 25)",
      "success": "oklch(0.696 0.17 162.48)",
      "success-foreground": "oklch(0.21 0.04 163)",
      "warning": "oklch(0.769 0.188 70.08)",
      "warning-foreground": "oklch(0.22 0.04 60)",
      "info": "oklch(0.7 0.14 237)",
      "info-foreground": "oklch(0.2 0.04 240)"
    }
  },
  "meta": {
    "project": "openlmis",
    "height": "238px"
  },
  "categories": [
    "forms",
    "auth"
  ],
  "type": "registry:component"
}
