{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "password-strength",
  "title": "Password Strength",
  "description": "A password strength meter for a sign-up, registration, change-password or reset-password form — the bar under the password field that says Weak or Strong, plus one line saying why. It scores how many guesses a password would survive rather than ticking off one uppercase, one number, one symbol: composition rules push people toward Password1!, which is guessed instantly, and reject correct horse battery staple, which is not. NIST SP 800-63B says the same — screen against known-bad passwords and let length do the work. It detects the things that make a password look random without being random: entries from a built-in list of the passwords that top every breach dump (folded through leet substitutions, so P@ssw0rd is found where password is, and unaffected by capitalisation), repeated characters, runs through the alphabet or the digits, runs along a keyboard row, and years and dates. The check hand-rolled meters always miss is userInputs: pass the email, username, display name or your product name and Acme2026! stops scoring as strong on acme.com — including the joined-up forms that separators hide, so Acme Co catches acmeco. Pass blocklist to add your own breach list on top; the built-in one is deliberately small, because a real one is megabytes and belongs behind an API. estimatePasswordStrength is exported on its own, pure and synchronous, so the same score that draws the meter can disable your submit button or drive a zod refine — no async, no 800 kB zxcvbn bundle, no dependencies at all. Accessibility is the other half: the bars are a role=\"meter\" with aria-valuetext, and only the band name sits in the aria-live region, so a screen reader hears \"Weak\" once when the password crosses a band instead of being read to on every keystroke — which is what an aria-live wrapped around the whole widget does. The advice line is tied to the meter with aria-describedby instead — give the component an id and it derives one for the advice and wires it up; without an id the advice is still read, just in document order rather than together with the meter. Warnings and suggestions come back as stable codes with an overridable message table, so the meter translates. It uses no hooks, so it renders in a server component and needs no \"use client\" of its own. Official shadcn/ui has nothing for passwords — no meter, no blocklist, no scorer; its input is a bare element and field and input-group are assembly kits with no logic in them. It scores a password but does not collect one: the field it sits under is this registry’s `password-input`, which is the same bare element with the show/hide eye and its accessible naming already wired. Pass this component that field’s value and the two compose into the whole password row.",
  "files": [
    {
      "path": "registry/ui/password-strength.tsx",
      "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/**\n * Why this is not a checklist of \"one uppercase, one digit, one symbol\":\n * composition rules push people toward `Password1!`, which is trivially\n * guessable, while rejecting `correct horse battery staple`, which is not.\n * NIST SP 800-63B says the same thing — screen against known-bad passwords and\n * let length do the work. So the score here is an estimate of how many guesses\n * the password would survive, and the only hard rule is a minimum length.\n */\n\n/** Why a password scored the way it did. Stable codes so the text can be translated. */\nexport type PasswordWarning =\n  | \"too-short\"\n  | \"common\"\n  | \"user-input\"\n  | \"repeat\"\n  | \"sequence\"\n  | \"keyboard\"\n  | \"year\"\n\n/** What to do about it. Stable codes so the text can be translated. */\nexport type PasswordSuggestion =\n  | \"longer\"\n  | \"passphrase\"\n  | \"avoid-common\"\n  | \"avoid-personal\"\n  | \"avoid-repeat\"\n  | \"avoid-sequence\"\n  | \"avoid-year\"\n\nexport type PasswordFeedbackCode = PasswordWarning | PasswordSuggestion\n\nexport interface PasswordStrengthOptions {\n  /**\n   * Things a guesser already knows about this user or this site: their email,\n   * username, display name, the product name. A password built out of them is\n   * weak however random it looks, and this is the check hand-rolled meters\n   * always miss.\n   */\n  userInputs?: string[]\n  /**\n   * Extra passwords to treat as known-bad, most guessable first — e.g. a slice\n   * of a breach list you ship or fetch. Merged with the small built-in list.\n   */\n  blocklist?: string[]\n  /** Below this the score is capped at 1, whatever the estimate says (default 8). */\n  minLength?: number\n}\n\nexport interface PasswordStrengthResult {\n  /** 0 very weak … 4 very strong. */\n  score: 0 | 1 | 2 | 3 | 4\n  /** log10 of the estimated number of guesses needed. */\n  guessesLog10: number\n  /** Length in code points, so an emoji counts as one character. */\n  length: number\n  /** The single biggest weakness, or null when nothing cheap was found. */\n  warning: PasswordWarning | null\n  /** At most two, most useful first. */\n  suggestions: PasswordSuggestion[]\n}\n\n/**\n * The passwords that appear at the top of every breach list, most common first —\n * the index is used as the rank, so `123456` costs fewer guesses than `monkey`.\n * Deliberately small: a real blocklist is megabytes and belongs behind an API,\n * which is what `blocklist` is for. This covers the ones that must never score\n * above \"very weak\" even when the page is offline.\n */\nconst COMMON =\n  \"123456 password 123456789 12345678 12345 qwerty 1234567 111111 1234567890 123123 abc123 1234 password1 iloveyou 000000 qwerty123 1q2w3e4r admin letmein welcome monkey dragon sunshine princess football baseball 654321 shadow master jennifer 111111111 superman qwertyuiop 123321 mustang 1qaz2wsx zaq12wsx asdfghjkl michael computer whatever passw0rd trustno1 batman jordan23 harley robert matthew daniel andrew lakers andrea buster joshua hunter ranger tigger soccer hockey killer george charlie dallas jessica pepper 1111 austin william golfer summer heather hammer yankees maggie biteme enter ashley thunder cowboy silver richard orange merlin michelle corvette bigdog cheese 121212 patrick martin freedom ginger nicole sparky yellow camaro secret falcon taylor 131313 hello scooter please porsche guitar chelsea black diamond nascar jackson cameron amanda wizard money phoenix mickey bailey knight iceman tigers purple dakota aaaaaa player morgan starwars boomer cowboys edward charles booboo coffee bulldog ncc1701 rabbit peanut johnny gandalf spanky winter brandy compaq carlos tennis james mike brandon fender anthony cookie chicken maverick chicago joseph diablo 666666 willie chris panther yamaha justin banana driver marine angels fishing david maddog wilson captain bigdaddy bronco voyager rangers birdie trouble white topgun green magic rachel slayer scott 2000 asdf video london 7777777 marlboro srinivas internet action carter jasper monster teresa jeremy 11111111 bill crystal peter pookie rascal stupid shannon murphy frank hannah dave eagle1 11111 mother nathan raiders steve forever angel viking root guest changeme qwe123 123qwe 1q2w3e 987654321 555555 abcd1234 welcome123 letmein123 iloveyou1 password123 test123 987654 112233 696969 pokemon starwars1 samsung google flower asdfgh zxcvbnm qwerty1 princess1 sunshine1\"\n    .split(\" \")\n\n/**\n * QWERTY rows, unshifted. A run along one of these (`asdfgh`, `zxcvbn`) reads as\n * random but is one of the first things a cracker tries.\n */\nconst KEYBOARD_ROWS = [\n  \"`1234567890-=\",\n  \"qwertyuiop[]\\\\\",\n  \"asdfghjkl;'\",\n  \"zxcvbnm,./\",\n]\n\n/** The substitutions people believe make a word unguessable. They do not. */\nconst LEET: Record<string, string> = {\n  \"0\": \"o\",\n  \"1\": \"l\",\n  \"2\": \"z\",\n  \"3\": \"e\",\n  \"4\": \"a\",\n  \"5\": \"s\",\n  \"6\": \"g\",\n  \"7\": \"t\",\n  \"8\": \"b\",\n  \"9\": \"g\",\n  \"@\": \"a\",\n  $: \"s\",\n  \"!\": \"i\",\n  \"|\": \"l\",\n  \"+\": \"t\",\n  \"(\": \"c\",\n}\n\nconst unleet = (s: string) => s.replace(/[0-9@$!|+(]/g, (c) => LEET[c] ?? c)\n\n/**\n * How many characters a brute-force attack would have to try per position. The\n * classes are added rather than maxed, because mixing them is what widens the\n * alphabet — one alphabet per class present, not per character.\n */\nfunction alphabetSize(s: string) {\n  let n = 0\n  if (/[a-z]/.test(s)) n += 26\n  if (/[A-Z]/.test(s)) n += 26\n  if (/[0-9]/.test(s)) n += 10\n  // Printable ASCII that is not a letter or a digit: the 33 punctuation keys.\n  if (/[\\x20-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\x7e]/.test(s)) n += 33\n  // Anything above ASCII. A flat 100 rather than \"all of Unicode\", because a\n  // person reaching past ASCII picks from their own keyboard, not from 150,000\n  // code points.\n  if (/[^\\x00-\\x7f]/.test(s)) n += 100\n  return Math.max(n, 1)\n}\n\n/**\n * `password` -> 1 guess, `Password` -> a few, `pAsSwOrD` -> more. Capitalising\n * the first letter or shouting the whole word are the two variants everyone\n * tries first, so they are nearly free; anything else is worth a little.\n */\nfunction caseFactor(token: string) {\n  if (!/[A-Z]/.test(token)) return 1\n  if (/^[^a-z]*$/.test(token)) return 2\n  if (/^.[^A-Z]*$/.test(token)) return 2\n  return 8\n}\n\ninterface Match {\n  /** Start index, inclusive, in code points. */\n  i: number\n  /** End index, exclusive. */\n  j: number\n  warning: PasswordWarning\n  /** log10 of the guesses this span costs on its own. */\n  cost: number\n}\n\nconst log10 = (n: number) => Math.log10(Math.max(n, 1))\n\n/**\n * Every span of the password that a guesser gets cheaply. Overlaps are fine and\n * expected — the segmentation below picks whichever combination is cheapest,\n * which is the attacker's job, not ours.\n */\nfunction findMatches(\n  cps: string[],\n  ranked: Map<string, number>,\n  context: Set<string>,\n  longestWord: number\n): Match[] {\n  const out: Match[] = []\n  const n = cps.length\n\n  // Dictionary and context. Both the literal text and its de-leeted form are\n  // looked up, so `P@ssw0rd` is found where `password` is.\n  //\n  // Spans longer than the longest word anyone is looking for cannot match, and\n  // skipping them is what keeps this off the critical path: it runs on every\n  // keystroke of a controlled input, and the unbounded version cost 22ms on a\n  // pasted 128-character string — long enough to drop frames while typing.\n  for (let i = 0; i < n; i++) {\n    let token = \"\"\n    const last = Math.min(n, i + longestWord)\n    for (let j = i; j < last; j++) {\n      token += cps[j]\n      if (token.length < 3) continue\n      const lower = token.toLowerCase()\n      const plain = ranked.get(lower)\n      const folded = ranked.get(unleet(lower))\n      const rank = Math.min(plain ?? Infinity, folded ?? Infinity)\n      const leetFactor = plain === undefined && folded !== undefined ? 4 : 1\n      if (Number.isFinite(rank)) {\n        out.push({\n          i,\n          j: j + 1,\n          warning: \"common\",\n          cost: log10(rank * caseFactor(token) * leetFactor),\n        })\n      }\n      if (context.has(lower) || context.has(unleet(lower))) {\n        // Known to whoever is attacking this account, so effectively free.\n        out.push({ i, j: j + 1, warning: \"user-input\", cost: log10(caseFactor(token)) })\n      }\n    }\n  }\n\n  // Maximal runs of one repeated character.\n  for (let i = 0; i < n; ) {\n    let j = i + 1\n    while (j < n && cps[j] === cps[i]) j++\n    if (j - i >= 3) {\n      out.push({\n        i,\n        j,\n        warning: \"repeat\",\n        cost: log10(alphabetSize(cps[i]) * (j - i)),\n      })\n    }\n    i = j\n  }\n\n  // Maximal runs stepping by one through the alphabet or the digits, either way.\n  for (let i = 0; i < n; ) {\n    let j = i + 1\n    let step = 0\n    while (j < n && sameClass(cps[i], cps[j])) {\n      const delta = cps[j].codePointAt(0)! - cps[j - 1].codePointAt(0)!\n      if (delta !== 1 && delta !== -1) break\n      if (step === 0) step = delta\n      else if (delta !== step) break\n      j++\n    }\n    if (j - i >= 3) {\n      const base = /[0-9]/.test(cps[i]) ? 10 : 26\n      out.push({ i, j, warning: \"sequence\", cost: log10(base * (j - i) * 2) })\n    }\n    i = Math.max(j - 1, i + 1)\n  }\n\n  // Runs along one row of the keyboard, either direction.\n  for (let i = 0; i < n; i++) {\n    for (const row of KEYBOARD_ROWS) {\n      let j = i + 1\n      let step = 0\n      while (j < n) {\n        const a = row.indexOf(cps[j - 1].toLowerCase())\n        const b = row.indexOf(cps[j].toLowerCase())\n        if (a < 0 || b < 0) break\n        const delta = b - a\n        if (delta !== 1 && delta !== -1) break\n        if (step === 0) step = delta\n        else if (delta !== step) break\n        j++\n      }\n      if (j - i >= 3) {\n        out.push({ i, j, warning: \"keyboard\", cost: log10(10 * (j - i) * 2) })\n      }\n    }\n  }\n\n  // Digit runs people pick from a calendar rather than at random. A year is the\n  // common case (`Acme2026!`); six or eight digits are dated well below their\n  // brute-force cost because a date is what they nearly always are. Where that\n  // guess is wrong the password is only reported as weaker than it is, which is\n  // the safe direction for a meter to be wrong in.\n  for (let i = 0; i < n; ) {\n    let j = i\n    while (j < n && /[0-9]/.test(cps[j])) j++\n    const len = j - i\n    if (len === 4) {\n      const year = Number(cps.slice(i, j).join(\"\"))\n      if (year >= 1900 && year <= 2099) {\n        out.push({ i, j, warning: \"year\", cost: log10(200) })\n      }\n    } else if (len === 6 || len === 8) {\n      out.push({ i, j, warning: \"year\", cost: log10(36500) })\n    }\n    i = Math.max(j, i + 1)\n  }\n\n  return out\n}\n\n/** Two characters are comparable as a sequence only within one alphabet. */\nfunction sameClass(a: string, b: string) {\n  if (/[0-9]/.test(a)) return /[0-9]/.test(b)\n  if (/[a-z]/.test(a)) return /[a-z]/.test(b)\n  if (/[A-Z]/.test(a)) return /[A-Z]/.test(b)\n  return false\n}\n\n/**\n * Only the first 128 code points are segmented. Beyond that the cost is already\n * astronomical and the O(n²) search is not worth running on a pasted file.\n */\nconst SCORED_LENGTH = 128\n\n/**\n * Estimate how many guesses a password would survive. Pure and synchronous, so\n * it can gate a submit button as well as drive the meter:\n *\n * ```ts\n * const { score } = estimatePasswordStrength(password, { userInputs: [email] })\n * <Button disabled={score < 2}>Create account</Button>\n * ```\n */\nexport function estimatePasswordStrength(\n  password: string,\n  options: PasswordStrengthOptions = {}\n): PasswordStrengthResult {\n  const { userInputs = [], blocklist = [], minLength = 8 } = options\n  const text = typeof password === \"string\" ? password : \"\"\n  const all = Array.from(text)\n  const cps = all.slice(0, SCORED_LENGTH)\n  const n = cps.length\n\n  if (n === 0) {\n    return { score: 0, guessesLog10: 0, length: 0, warning: null, suggestions: [] }\n  }\n\n  // Rank is a word's position in whichever list ranks it highest, not its\n  // position in the two lists concatenated. Concatenating meant that passing a\n  // large blocklist pushed the built-in entries down past its length — hand it\n  // ten thousand breached passwords and `password` stopped scoring as one.\n  const ranked = new Map<string, number>()\n  for (const list of [blocklist, COMMON]) {\n    for (let k = 0; k < list.length; k++) {\n      const word = String(list[k] ?? \"\").toLowerCase()\n      if (!word) continue\n      const rank = k + 1\n      const seen = ranked.get(word)\n      if (seen === undefined || rank < seen) ranked.set(word, rank)\n    }\n  }\n\n  // An email is not guessed whole — its pieces are. `jane.doe@acme.com` puts\n  // jane, doe and acme in play as much as the address itself, and so does\n  // janedoe, which is what the separators were hiding.\n  const context = new Set<string>()\n  for (const input of userInputs) {\n    const value = String(input ?? \"\").toLowerCase()\n    for (const part of [value, value.replace(/[^a-z0-9]+/g, \"\"), ...value.split(/[^a-z0-9]+/)]) {\n      if (part.length >= 3) context.add(part)\n    }\n  }\n\n  // Bound for the substring scan. Measured in UTF-16 units, which never\n  // undercounts code points, so no match can be skipped by it.\n  let longestWord = 0\n  for (const word of ranked.keys()) longestWord = Math.max(longestWord, word.length)\n  for (const word of context) longestWord = Math.max(longestWord, word.length)\n\n  const perChar = log10(alphabetSize(text))\n  const matches = findMatches(cps, ranked, context, longestWord)\n  const endingAt: Match[][] = Array.from({ length: n + 1 }, () => [])\n  for (const m of matches) endingAt[m.j].push(m)\n\n  // Cheapest way to spell the password out of the spans above, brute-forcing\n  // whatever is left over. Segment count is not penalised, so a password built\n  // of many cheap pieces is if anything under-rated — again, the safe direction.\n  const best = new Array<number>(n + 1).fill(Infinity)\n  const via = new Array<Match | null>(n + 1).fill(null)\n  const from = new Array<number>(n + 1).fill(0)\n  best[0] = 0\n  for (let i = 1; i <= n; i++) {\n    best[i] = best[i - 1] + perChar\n    from[i] = i - 1\n    for (const m of endingAt[i]) {\n      const total = best[m.i] + m.cost\n      if (total < best[i]) {\n        best[i] = total\n        from[i] = m.i\n        via[i] = m\n      }\n    }\n  }\n\n  const guessesLog10 = best[n] + (all.length - n) * perChar\n\n  let score: PasswordStrengthResult[\"score\"] =\n    guessesLog10 < 3 ? 0 : guessesLog10 < 6 ? 1 : guessesLog10 < 8 ? 2 : guessesLog10 < 10 ? 3 : 4\n\n  // The widest span the segmentation actually used — the part of the password\n  // that is carrying the least weight.\n  let weakest: Match | null = null\n  for (let i = n; i > 0; i = from[i]) {\n    const m = via[i]\n    if (m && (!weakest || m.j - m.i > weakest.j - weakest.i)) weakest = m\n  }\n\n  let warning: PasswordWarning | null = weakest ? weakest.warning : null\n  if (all.length < minLength) {\n    // Saying \"Fair\" about something the form is going to reject is worse than\n    // saying nothing, so length overrides the estimate rather than adding to it.\n    warning = \"too-short\"\n    if (score > 1) score = 1\n  }\n\n  const suggestions: PasswordSuggestion[] = []\n  const add = (s: PasswordSuggestion) => {\n    if (!suggestions.includes(s)) suggestions.push(s)\n  }\n  if (warning === \"too-short\") add(\"longer\")\n  else if (warning === \"common\") {\n    add(\"avoid-common\")\n    add(\"passphrase\")\n  } else if (warning === \"user-input\") add(\"avoid-personal\")\n  else if (warning === \"repeat\") add(\"avoid-repeat\")\n  else if (warning === \"sequence\" || warning === \"keyboard\") add(\"avoid-sequence\")\n  else if (warning === \"year\") add(\"avoid-year\")\n  if (score < 3) add(warning ? \"longer\" : \"passphrase\")\n\n  return {\n    score,\n    guessesLog10,\n    length: all.length,\n    warning,\n    suggestions: suggestions.slice(0, 2),\n  }\n}\n\n/** Band names, weakest first. Replace with your own to translate the meter. */\nexport const passwordStrengthLabels = [\n  \"Very weak\",\n  \"Weak\",\n  \"Fair\",\n  \"Strong\",\n  \"Very strong\",\n] as const\n\n/** English text for every feedback code. Pass `messages` to override any of them. */\nexport const passwordStrengthMessages: Record<PasswordFeedbackCode, string> = {\n  \"too-short\": \"This password is too short.\",\n  common: \"This is one of the most commonly used passwords.\",\n  \"user-input\": \"This looks like your name, your email, or this site.\",\n  repeat: \"Repeated characters like “aaa” are quick to guess.\",\n  sequence: \"Runs like “abc” or “123” are quick to guess.\",\n  keyboard: \"Keyboard patterns like “qwerty” are quick to guess.\",\n  year: \"Dates and years are quick to guess.\",\n  longer: \"Add a few more characters — length beats symbols.\",\n  passphrase: \"A few unrelated words are strong and easy to remember.\",\n  \"avoid-common\": \"Pick something that is not on every leaked-password list.\",\n  \"avoid-personal\": \"Avoid your name, your email, and this site’s name.\",\n  \"avoid-repeat\": \"Avoid repeating the same character.\",\n  \"avoid-sequence\": \"Avoid straight runs from the alphabet or the keyboard.\",\n  \"avoid-year\": \"Avoid birthdays and years.\",\n}\n\n/** Bar colour per score, weakest first. */\nconst BAR_CLASSES = [\n  \"bg-destructive\",\n  \"bg-destructive\",\n  \"bg-amber-500 dark:bg-amber-400\",\n  \"bg-emerald-600 dark:bg-emerald-500\",\n  \"bg-emerald-600 dark:bg-emerald-500\",\n] as const\n\nconst TEXT_CLASSES = [\n  \"text-destructive\",\n  \"text-destructive\",\n  \"text-amber-600 dark:text-amber-400\",\n  \"text-emerald-600 dark:text-emerald-400\",\n  \"text-emerald-600 dark:text-emerald-400\",\n] as const\n\ninterface PasswordStrengthProps\n  extends Omit<React.ComponentPropsWithoutRef<\"div\">, \"children\">,\n    PasswordStrengthOptions {\n  /** The password to score. Render this next to your own input; it never renders the value. */\n  value: string\n  /** Show the band name beside the bars (default true). */\n  showLabel?: boolean\n  /** Show one line of advice under the bars (default true). */\n  showFeedback?: boolean\n  /** Band names, weakest first — pass translated strings here. */\n  labels?: readonly string[]\n  /** Override any of the feedback strings, e.g. to translate them. */\n  messages?: Partial<Record<PasswordFeedbackCode, string>>\n  /** Accessible name for the meter (default \"Password strength\"). */\n  \"aria-label\"?: string\n}\n\n/**\n * A strength meter for a password field. Reads `value`, never renders it.\n *\n * It uses no hooks, so it works in a server component and needs no `\"use client\"`\n * of its own — the client boundary stays on whatever owns the password state.\n *\n * The band name is the only thing in the live region, so a screen reader hears\n * \"Weak\" once when the password crosses into weak and stays quiet for the\n * keystrokes in between. The advice line is deliberately left out of it — it can\n * change on any keystroke, and announcing it would talk over the typing. Give\n * the component an `id` and the advice is tied to the meter with\n * aria-describedby as well; without one it is read in document order.\n */\nexport const PasswordStrength = React.forwardRef<\n  HTMLDivElement,\n  PasswordStrengthProps\n>(function PasswordStrength(\n  {\n    className,\n    value,\n    userInputs,\n    blocklist,\n    minLength,\n    showLabel = true,\n    showFeedback = true,\n    labels = passwordStrengthLabels,\n    messages,\n    \"aria-label\": ariaLabel = \"Password strength\",\n    id,\n    ...props\n  },\n  ref\n) {\n  const text = typeof value === \"string\" ? value : \"\"\n  const { score, warning, suggestions } = estimatePasswordStrength(text, {\n    userInputs,\n    blocklist,\n    minLength,\n  })\n  const empty = text.length === 0\n  const label = labels[score] ?? passwordStrengthLabels[score]\n  const say = (code: PasswordFeedbackCode) =>\n    messages?.[code] ?? passwordStrengthMessages[code]\n\n  const advice = empty\n    ? \"\"\n    : [warning, suggestions[0]]\n        .filter((code): code is PasswordFeedbackCode => Boolean(code))\n        .map(say)\n        .join(\" \")\n\n  // Both bands at the bottom light one bar: \"very weak\" and \"weak\" differ in what\n  // they are called, not in how full the meter looks.\n  const filled = empty ? 0 : Math.max(score, 1)\n\n  // Only wired up when the caller gave the component an id to derive one from; a\n  // dangling aria-describedby is worse than none. Without it the advice is still\n  // read out, just in document order rather than together with the meter.\n  const showAdvice = showFeedback && advice !== \"\"\n  const adviceId = id && showAdvice ? `${id}-advice` : undefined\n\n  return (\n    <div ref={ref} id={id} className={cn(\"flex flex-col gap-1.5\", className)} {...props}>\n      <div\n        role=\"meter\"\n        aria-label={ariaLabel}\n        aria-valuemin={0}\n        aria-valuemax={4}\n        aria-valuenow={score}\n        aria-valuetext={empty ? \"No password entered\" : label}\n        aria-describedby={adviceId}\n        className=\"flex h-1.5 w-full gap-1\"\n      >\n        {[0, 1, 2, 3].map((i) => (\n          <span\n            key={i}\n            className={cn(\n              \"h-full flex-1 rounded-full bg-muted transition-colors\",\n              i < filled && BAR_CLASSES[score]\n            )}\n          />\n        ))}\n      </div>\n\n      {showLabel ? (\n        <span\n          aria-hidden=\"true\"\n          className={cn(\n            \"min-h-4 text-xs font-medium\",\n            empty ? \"text-muted-foreground\" : TEXT_CLASSES[score]\n          )}\n        >\n          {empty ? \"\" : label}\n        </span>\n      ) : null}\n\n      {/* The only thing announced, and it changes only when the band does. */}\n      <span className=\"sr-only\" aria-live=\"polite\">\n        {empty ? \"\" : `${ariaLabel}: ${label}`}\n      </span>\n\n      {showAdvice ? (\n        <p id={adviceId} className=\"text-xs text-muted-foreground\">\n          {advice}\n        </p>\n      ) : null}\n    </div>\n  )\n})\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}