{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "password-strength",
  "type": "registry:ui",
  "title": "Password strength",
  "description": "Show how strong a new password is while it is typed.",
  "categories": [
    "inputs"
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://mcp.xzero.co.uk/r/xzero-motion-tokens.json"
  ],
  "files": [
    {
      "path": "registry/components/password-strength/password-strength.tsx",
      "type": "registry:component",
      "target": "@components/xzero/password-strength/password-strength.tsx",
      "content": "\"use client\";\n\nimport { forwardRef, useEffect, useId, useRef, useState, useSyncExternalStore } from \"react\";\nimport type { InputHTMLAttributes } from \"react\";\nimport { AnimatePresence, animate, motion, useMotionValue, useReducedMotion } from \"motion/react\";\nimport type { AnimationPlaybackControls, Transition, Variants } from \"motion/react\";\nimport { motionTokens } from \"../lib/motion-tokens\";\nimport styles from \"./password-strength.module.css\";\n\nexport interface PasswordRule {\n  id: string;\n  label: string;\n  test: (password: string) => boolean;\n  /** Characters still missing. Shown as a rolling count beside the rule while it is unmet. */\n  remaining?: (password: string) => number;\n}\n\nexport interface PasswordStrengthResult {\n  level: 0 | 1 | 2 | 3 | 4;\n  label: string;\n  met: string[];\n  /** True while the password is shorter than the short length, so the meter holds at its first step. */\n  short?: boolean;\n}\n\n/** The strength words, one per meter step. Override them to translate the meter. */\nexport interface PasswordStrengthLevels {\n  tooShort: string;\n  weak: string;\n  fair: string;\n  good: string;\n  strong: string;\n}\n\n/** Every user-facing string in PasswordStrength. All have English defaults; pass only the ones you change. */\nexport interface PasswordStrengthMessages {\n  levels: PasswordStrengthLevels;\n  /** Name of the show password toggle. It is a pressed toggle (aria-pressed), so the name stays the same in both states. */\n  showPassword: string;\n  /** Accessible name of the strength meter. */\n  meterLabel: string;\n  /** Meter value text while the field is empty. */\n  noPassword: string;\n  /** Accessible name of the requirements list. */\n  rulesLabel: string;\n  /** Read after a rule's label for screen readers, including any leading punctuation. */\n  ruleMet: string;\n  ruleNotMet: string;\n  /** Words around the rolling count beside an unmet rule, as in \"4 more\". */\n  remainingPrefix: string;\n  remainingSuffix: string;\n  /** The polite live-region summary read after each change. */\n  summary: (result: { label: string; met: number; total: number }) => string;\n}\n\nexport const defaultPasswordStrengthMessages: PasswordStrengthMessages = {\n  levels: { tooShort: \"Too short\", weak: \"Weak\", fair: \"Fair\", good: \"Good\", strong: \"Strong\" },\n  showPassword: \"Show password\",\n  meterLabel: \"Password strength\",\n  noPassword: \"No password yet\",\n  rulesLabel: \"Password requirements\",\n  ruleMet: \", met\",\n  ruleNotMet: \", not met\",\n  remainingPrefix: \"\",\n  remainingSuffix: \"more\",\n  summary: ({ label, met, total }) => `Strength: ${label}. ${met} of ${total} requirements met.`,\n};\n\n/** Partial messages, with partial strength words, for the `messages` prop. */\nexport type PasswordStrengthMessagesInput = Partial<Omit<PasswordStrengthMessages, \"levels\">> & { levels?: Partial<PasswordStrengthLevels> };\n\n/**\n * A new password field that shows how strong the password is while it is typed: four segments fill and change tone,\n * each rule checks off with a drawn tick, and the strength word morphs in place. Use it when creating or changing a password;\n * use a plain password field for sign in. Scoring happens on the device, nothing is sent anywhere.\n */\nexport interface PasswordStrengthProps extends Omit<InputHTMLAttributes<HTMLInputElement>, \"type\" | \"value\" | \"defaultValue\" | \"children\"> {\n  label: string;\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string, strength: PasswordStrengthResult) => void;\n  /** Rules to check. Strength is the share of rules met, spread over four steps. */\n  rules?: PasswordRule[];\n  /** Error copy tied to the field. The field shakes once each time a new error appears. */\n  error?: string;\n  revealed?: boolean;\n  onRevealedChange?: (revealed: boolean) => void;\n  /** Translated or reworded copy. Every string has an English default; see PasswordStrengthMessages. */\n  messages?: PasswordStrengthMessagesInput;\n  /** Below this many characters the meter holds at its first step and reads \"Too short\". Defaults to 8. */\n  shortLength?: number;\n  /** How characters are counted for shortLength. Defaults to code points, so an emoji counts once; pass `password => password.length` to count UTF-16 units like a server might. */\n  countCharacters?: (password: string) => number;\n}\n\n/** Counts code points, so an emoji or other astral character counts as one. */\nexport const countCodePoints = (password: string) => Array.from(password).length;\n\n/** Labels for the built-in rules. `length` receives the minimum length. */\nexport interface PasswordRuleLabels {\n  length: (minLength: number) => string;\n  case: string;\n  number: string;\n  symbol: string;\n}\n\nexport interface PasswordRuleOptions {\n  /** Characters the length rule asks for. Defaults to 12. */\n  minLength?: number;\n  /** Translated or reworded rule labels. */\n  labels?: Partial<PasswordRuleLabels>;\n  /** How the length rule counts characters. Defaults to code points. */\n  countCharacters?: (password: string) => number;\n}\n\nconst defaultRuleLabels: PasswordRuleLabels = {\n  length: minLength => `At least ${minLength} characters`,\n  case: \"Upper and lowercase letters\",\n  number: \"At least one number\",\n  symbol: \"At least one symbol\",\n};\n\n/** The built-in rules (length, mixed case, number, symbol) with your own minimum length, labels and character count. */\nexport function createPasswordRules({ minLength = 12, labels, countCharacters = countCodePoints }: PasswordRuleOptions = {}): PasswordRule[] {\n  const copy = { ...defaultRuleLabels, ...labels };\n  return [\n    { id: \"length\", label: copy.length(minLength), test: password => countCharacters(password) >= minLength, remaining: password => Math.max(0, minLength - countCharacters(password)) },\n    { id: \"case\", label: copy.case, test: password => /\\p{Ll}/u.test(password) && /\\p{Lu}/u.test(password) },\n    { id: \"number\", label: copy.number, test: password => /\\p{N}/u.test(password) },\n    { id: \"symbol\", label: copy.symbol, test: password => /[^\\p{L}\\p{N}\\s]/u.test(password) },\n  ];\n}\n\nexport const defaultPasswordRules: PasswordRule[] = createPasswordRules();\n\nexport interface EstimateStrengthOptions {\n  /** Below this many characters a password stays at the first step, however varied it is. Defaults to 8. */\n  shortLength?: number;\n  /** Strength words for the result's label. */\n  levels?: Partial<PasswordStrengthLevels>;\n  /** How characters are counted for shortLength. Defaults to code points. */\n  countCharacters?: (password: string) => number;\n}\n\n/** Scores a password on the device: one step per share of rules met, capped at the first step while it is shorter than the short length (8 by default). */\nexport function estimateStrength(password: string, rules: PasswordRule[] = defaultPasswordRules, { shortLength = 8, levels, countCharacters = countCodePoints }: EstimateStrengthOptions = {}): PasswordStrengthResult {\n  if (!password) return { level: 0, label: \"\", met: [] };\n  const words = { ...defaultPasswordStrengthMessages.levels, ...levels };\n  const met = rules.filter(rule => rule.test(password)).map(rule => rule.id);\n  if (countCharacters(password) < shortLength) return { level: 1, label: words.tooShort, met, short: true };\n  const level = Math.min(4, Math.max(1, Math.round((met.length / Math.max(rules.length, 1)) * 4))) as 1 | 2 | 3 | 4;\n  return { level, label: [words.weak, words.fair, words.good, words.strong][level - 1] ?? words.weak, met };\n}\n\nconst enter = [...motionTokens.ease.enter] as [number, number, number, number];\nconst standard = [...motionTokens.ease.standard] as [number, number, number, number];\nconst subscribe = () => () => {};\n/** False on the server and during hydration, so reduced motion never changes the first client render. */\nconst useHydrated = () => useSyncExternalStore(subscribe, () => true, () => false);\n\n/** The word rises when strength improves and drops when it falls, so the direction reads without looking at the meter. */\nconst rise: Variants = {\n  enter: (direction: number) => ({ opacity: 0, y: `${.3 * direction}em`, filter: `blur(${motionTokens.blur.soft}px)` }),\n  center: { opacity: 1, y: \"0em\", filter: \"blur(0px)\", transition: { duration: motionTokens.duration.standard, ease: enter } },\n  exit: (direction: number) => ({ opacity: 0, y: `${-.3 * direction}em`, filter: `blur(${motionTokens.blur.subtle}px)`, transition: { duration: motionTokens.duration.fast, ease: standard } }),\n};\nconst fade: Variants = { enter: { opacity: 0 }, center: { opacity: 1, transition: { duration: motionTokens.duration.fast } }, exit: { opacity: 0, transition: { duration: motionTokens.duration.instant } } };\n/** Digits turn like a counter: a shrinking count drops in from above, a growing one rises from below. */\nconst roll: Variants = {\n  enter: (direction: number) => ({ opacity: 0, y: `${.45 * direction}em`, filter: `blur(${motionTokens.blur.subtle}px)` }),\n  center: { opacity: 1, y: \"0em\", filter: \"blur(0px)\", transition: { y: motionTokens.spring.snappy, opacity: { duration: motionTokens.duration.fast }, filter: { duration: motionTokens.duration.fast } } },\n  exit: (direction: number) => ({ opacity: 0, y: `${-.45 * direction}em`, filter: `blur(${motionTokens.blur.subtle}px)`, transition: { duration: motionTokens.duration.fast, ease: standard } }),\n};\n\nfunction RollingNumber({ value, reduced }: { value: number; reduced: boolean }) {\n  const [track, setTrack] = useState({ value, direction: 1 });\n  if (track.value !== value) setTrack({ value, direction: value > track.value ? 1 : -1 });\n  const digits = String(value).split(\"\");\n  // Columns are keyed by place value, so 10 → 9 lets the tens column narrow away while the ones column turns.\n  return <span className={styles.number}>\n    <AnimatePresence initial={false} custom={track.direction}>\n      {digits.map((digit, index) => <motion.span key={digits.length - 1 - index} className={styles.place} initial={{ width: 0, opacity: 0 }} animate={{ width: \"auto\", opacity: 1 }} exit={{ width: 0, opacity: 0 }} transition={reduced ? { duration: 0 } : { width: motionTokens.spring.morph, opacity: { duration: motionTokens.duration.fast } }}>\n        <AnimatePresence mode=\"popLayout\" initial={false} custom={track.direction}>\n          <motion.span key={digit} className={styles.digit} custom={track.direction} variants={reduced ? fade : roll} initial=\"enter\" animate=\"center\" exit=\"exit\">{digit}</motion.span>\n        </AnimatePresence>\n      </motion.span>)}\n    </AnimatePresence>\n  </span>;\n}\n\n/** One eye that a slash draws across, cutting the outline beneath it, instead of swapping two icons. */\nfunction EyeMorph({ slashed, reduced }: { slashed: boolean; reduced: boolean }) {\n  const maskId = `eye-${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n  const slash = { pathLength: slashed ? 1 : 0, opacity: slashed ? 1 : 0 };\n  const transition: Transition = reduced ? { duration: 0 } : { pathLength: { duration: motionTokens.duration.standard, ease: standard }, opacity: { duration: motionTokens.duration.instant } };\n  return <svg width={18} height={18} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.75} strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n    <mask id={maskId} maskUnits=\"userSpaceOnUse\" x=\"0\" y=\"0\" width=\"24\" height=\"24\"><rect width=\"24\" height=\"24\" fill=\"white\" stroke=\"none\" /><motion.path d=\"M3 3l18 18\" stroke=\"black\" strokeWidth={5} initial={false} animate={slash} transition={transition} /></mask>\n    <g mask={`url(#${maskId})`}><path d=\"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0\" /><circle cx=\"12\" cy=\"12\" r=\"3\" /></g>\n    <motion.path d=\"M3 3l18 18\" initial={false} animate={slash} transition={transition} />\n  </svg>;\n}\n\n/** The tint grows under the tick while the tick draws from its short stroke; unchecking retracts it faster than it drew. */\nfunction RuleMark({ met, delay, reduced }: { met: boolean; delay: number; reduced: boolean }) {\n  return <span className={styles.mark} aria-hidden=\"true\">\n    <motion.span className={styles.markFill} initial={false} animate={{ scale: met ? 1 : .5, opacity: met ? 1 : 0 }} transition={reduced ? { duration: 0 } : { scale: { ...motionTokens.spring.snappy, delay }, opacity: { duration: met ? motionTokens.duration.fast : motionTokens.duration.instant, delay } }} />\n    <svg className={styles.tick} viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.75} strokeLinecap=\"round\" strokeLinejoin=\"round\">\n      <motion.path d=\"M4.75 8.25 7 10.5l4.25-4.75\" initial={false} animate={{ pathLength: met ? 1 : 0, opacity: met ? 1 : 0 }} transition={reduced ? { duration: 0 } : met ? { pathLength: { duration: motionTokens.duration.standard, ease: enter, delay: delay + .06 }, opacity: { duration: .05, delay: delay + .06 } } : { pathLength: { duration: motionTokens.duration.fast, ease: standard }, opacity: { duration: motionTokens.duration.fast, delay: .06 } }} />\n    </svg>\n  </span>;\n}\n\n/** Error copy opens its row on a spring, then the words settle in. The row follows the measured copy, so a wrap never snaps. */\nfunction ErrorRow({ id, text, reduced }: { id: string; text: string; reduced: boolean }) {\n  const copy = useRef<HTMLSpanElement>(null);\n  const [height, setHeight] = useState<number | \"auto\">(\"auto\");\n  useEffect(() => {\n    const node = copy.current;\n    if (!node || typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver(([entry]) => { if (entry) setHeight(entry.borderBoxSize?.[0]?.blockSize ?? node.offsetHeight); });\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, []);\n  return <motion.span className={styles.errorSlot} initial={{ height: 0, opacity: 0 }} animate={{ height, opacity: 1 }} exit={{ height: 0, opacity: 0, transition: reduced ? { duration: 0 } : { height: motionTokens.spring.smooth, opacity: { duration: motionTokens.duration.instant } } }} transition={reduced ? { duration: 0 } : { height: motionTokens.spring.smooth, opacity: { duration: motionTokens.duration.fast } }}>\n    <span ref={copy} id={id} className={styles.error}>\n      <AnimatePresence mode=\"popLayout\" initial={false} custom={1}>\n        <motion.span key={text} className={styles.errorText} custom={1} variants={reduced ? fade : rise} initial=\"enter\" animate=\"center\" exit=\"exit\">{text}</motion.span>\n      </AnimatePresence>\n    </span>\n  </motion.span>;\n}\n\nexport const PasswordStrength = forwardRef<HTMLInputElement, PasswordStrengthProps>(function PasswordStrength({ label, value: valueProp, defaultValue = \"\", onValueChange, onChange, rules = defaultPasswordRules, error, revealed: revealedProp, onRevealedChange, messages: messagesProp, shortLength, countCharacters, id, className, ...props }, ref) {\n  const messages: PasswordStrengthMessages = { ...defaultPasswordStrengthMessages, ...messagesProp, levels: { ...defaultPasswordStrengthMessages.levels, ...messagesProp?.levels } };\n  const options: EstimateStrengthOptions = { shortLength, levels: messages.levels, countCharacters };\n  const generated = useId();\n  const controlId = id ?? generated;\n  const rulesId = `${controlId}-rules`, errorId = `${controlId}-error`;\n  const hydrated = useHydrated();\n  const prefersReduced = useReducedMotion();\n  const reduced = hydrated && !!prefersReduced;\n  const [internal, setInternal] = useState(defaultValue);\n  const value = valueProp ?? internal;\n  const [revealedInternal, setRevealedInternal] = useState(false);\n  const revealed = revealedProp ?? revealedInternal;\n  const strength = estimateStrength(value, rules, options);\n  const met = new Set(strength.met);\n\n  // The previous level and rule states tell which way the word moves and which segments and ticks join a staggered wave.\n  const rank = strength.short ? .5 : strength.level;\n  const metKey = strength.met.join(\" \");\n  const [track, setTrack] = useState({ rank, level: strength.level, previousLevel: strength.level, direction: 1, metKey, previousMet: metKey });\n  if (track.rank !== rank || track.metKey !== metKey) setTrack({ rank, level: strength.level, previousLevel: track.rank !== rank ? track.level : track.previousLevel, direction: track.rank === rank ? track.direction : rank > track.rank ? 1 : -1, metKey, previousMet: track.metKey !== metKey ? track.metKey : track.previousMet });\n  const previousMet = new Set(track.previousMet.split(\" \").filter(Boolean));\n  const changed = rules.filter(rule => met.has(rule.id) !== previousMet.has(rule.id)).map(rule => rule.id);\n\n  // Only a change after mount resolves the text, so the field never blurs in on first paint.\n  const [reveal, setReveal] = useState({ revealed, changed: false });\n  if (reveal.revealed !== revealed) setReveal({ revealed, changed: true });\n\n  const shake = useMotionValue(0);\n  const shaking = useRef<AnimationPlaybackControls | null>(null);\n  const lastError = useRef(error);\n  useEffect(() => {\n    const previous = lastError.current;\n    lastError.current = error;\n    if (!error || error === previous || reduced) return;\n    // A spring released with sideways velocity rings out on its own, the way a refused field shakes on a phone.\n    shaking.current?.stop();\n    shaking.current = animate(shake, 0, { type: \"spring\", velocity: -240, stiffness: 900, damping: 57, restDelta: .1 });\n  }, [error, reduced, shake]);\n\n  function toggleReveal() {\n    if (revealedProp === undefined) setRevealedInternal(!revealed);\n    onRevealedChange?.(!revealed);\n  }\n\n  const summary = strength.level ? messages.summary({ label: strength.label, met: strength.met.length, total: rules.length }) : \"\";\n  const describedBy = [props[\"aria-describedby\"], error ? errorId : undefined, rulesId].filter(Boolean).join(\" \");\n\n  return <div className={styles.field} data-level={strength.level}>\n    <label className={styles.label} htmlFor={controlId}>{label}</label>\n    <motion.div className={styles.shell} style={{ x: shake }}>\n      <input autoComplete=\"new-password\" autoCapitalize=\"off\" autoCorrect=\"off\" spellCheck={false} {...props} ref={ref} id={controlId} type={revealed ? \"text\" : \"password\"} value={value}\n        onChange={event => { const next = event.target.value; if (valueProp === undefined) setInternal(next); onChange?.(event); onValueChange?.(next, estimateStrength(next, rules, options)); }}\n        aria-invalid={error ? true : props[\"aria-invalid\"]} aria-describedby={describedBy} data-reveal={reveal.changed ? (revealed ? \"shown\" : \"hidden\") : undefined} className={[styles.input, className].filter(Boolean).join(\" \")} />\n      <button type=\"button\" className={styles.reveal} onClick={toggleReveal} aria-label={messages.showPassword} aria-pressed={revealed} aria-controls={controlId}><EyeMorph slashed={revealed} reduced={reduced} /></button>\n    </motion.div>\n    <AnimatePresence initial={false}>{error ? <ErrorRow key=\"error\" id={errorId} text={error} reduced={reduced} /> : null}</AnimatePresence>\n    <div className={styles.meterRow}>\n      <div className={styles.meter} role=\"meter\" aria-label={messages.meterLabel} aria-valuemin={0} aria-valuemax={4} aria-valuenow={strength.level} aria-valuetext={strength.level ? strength.label : messages.noPassword}>\n        {[0, 1, 2, 3].map(index => {\n          const on = index < strength.level;\n          // A pasted password fills left to right; clearing empties right to left.\n          const wave = on ? index - track.previousLevel : track.previousLevel - 1 - index;\n          return <span key={index} className={styles.segment}><motion.span className={styles.fill} initial={false} animate={{ x: on ? \"0%\" : \"-101%\" }} transition={reduced ? { duration: 0 } : { ...motionTokens.spring.smooth, delay: Math.max(0, wave) * (on ? .05 : .03) }} /></span>;\n        })}\n      </div>\n      <span className={styles.word} aria-hidden=\"true\">\n        <AnimatePresence mode=\"popLayout\" initial={false} custom={track.direction}>\n          {strength.level ? <motion.span key={strength.label} className={styles.wordText} custom={track.direction} variants={reduced ? fade : rise} initial=\"enter\" animate=\"center\" exit=\"exit\">{strength.label}</motion.span> : null}\n        </AnimatePresence>\n      </span>\n    </div>\n    <ul id={rulesId} className={styles.rules} aria-label={messages.rulesLabel}>\n      {rules.map(rule => {\n        const ok = met.has(rule.id);\n        const remaining = value && !ok ? rule.remaining?.(value) ?? 0 : 0;\n        const order = changed.indexOf(rule.id);\n        return <li key={rule.id} className={styles.rule} data-met={ok || undefined}>\n          <RuleMark met={ok} delay={order > 0 ? order * .05 : 0} reduced={reduced} />\n          <span className={styles.ruleLabel}>{rule.label}<span className={styles.srOnly}>{ok ? messages.ruleMet : messages.ruleNotMet}</span></span>\n          <span className={styles.remaining} aria-hidden=\"true\">\n            <AnimatePresence mode=\"popLayout\" initial={false} custom={1}>\n              {remaining > 0 ? <motion.span key=\"remaining\" className={styles.remainingText} custom={1} variants={reduced ? fade : rise} initial=\"enter\" animate=\"center\" exit=\"exit\">{messages.remainingPrefix ? `${messages.remainingPrefix} ` : null}<RollingNumber value={remaining} reduced={reduced} />{messages.remainingSuffix ? ` ${messages.remainingSuffix}` : null}</motion.span> : null}\n            </AnimatePresence>\n          </span>\n        </li>;\n      })}\n    </ul>\n    <span className={styles.srOnly} role=\"status\">{summary}</span>\n  </div>;\n});\n\nPasswordStrength.displayName = \"PasswordStrength\";\n\nexport default PasswordStrength;\n"
    },
    {
      "path": "registry/components/password-strength/password-strength.module.css",
      "type": "registry:component",
      "target": "@components/xzero/password-strength/password-strength.module.css",
      "content": "/* No row gap: a closing error row would still pay it. Each row carries its own space instead. */\n.field { --tone: var(--border-strong); display: grid; min-width: 0; }\n.field[data-level=\"1\"] { --tone: var(--danger); }\n.field[data-level=\"2\"] { --tone: var(--warning); }\n.field[data-level=\"3\"], .field[data-level=\"4\"] { --tone: var(--success); }\n.label { margin-bottom: var(--space-2); color: var(--foreground); font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); }\n/* The shell only transitions color; its sideways shake is a spring on transform, so the two never fight. */\n.shell { display: flex; min-height: var(--control-height-md); align-items: center; gap: var(--space-1); padding: 0 5px 0 var(--space-4); border: 1px solid var(--border-strong); border-radius: var(--radius-control); background: var(--surface); transition: border-color var(--duration-fast) var(--ease-standard); }\n@media (hover: hover) and (pointer: fine) { .shell:hover:not(:focus-within) { border-color: var(--text-muted); } }\n.shell:focus-within { border-color: var(--foreground); }\n.shell:has(.input[aria-invalid=\"true\"]) { border-color: var(--danger); }\n.input { width: 100%; min-width: 0; height: calc(var(--control-height-md) - 2px); padding: 0; border: 0; background: transparent; color: var(--foreground); font: inherit; font-size: var(--text-sm); letter-spacing: var(--tracking-body); outline: 0; }\n.input::placeholder { color: var(--text-muted); }\n/* Touch keyboards zoom into fields under 16px, so coarse pointers get the larger size. */\n@media (pointer: coarse) { .input { font-size: var(--text-base); } }\n/* Dots and characters resolve into each other instead of snapping. Swapping the keyframe name restarts it. */\n.input[data-reveal=\"shown\"] { animation: resolve-shown var(--duration-standard) var(--ease-enter); }\n.input[data-reveal=\"hidden\"] { animation: resolve-hidden var(--duration-standard) var(--ease-enter); }\n@keyframes resolve-shown { from { opacity: .35; filter: blur(2px); } }\n@keyframes resolve-hidden { from { opacity: .35; filter: blur(2px); } }\n.reveal { display: grid; width: 34px; height: 34px; flex: 0 0 auto; place-items: center; padding: 0; border: 0; border-radius: calc(var(--radius-control) - 5px); background: transparent; color: var(--text-muted); cursor: pointer; -webkit-tap-highlight-color: transparent; transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), transform var(--duration-spring) var(--ease-spring); }\n@media (hover: hover) and (pointer: fine) { .reveal:hover { background: var(--surface-muted); color: var(--foreground); } }\n.reveal:active { transform: scale(.94); transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), transform 100ms var(--ease-standard); }\n.reveal[aria-pressed=\"true\"] { color: var(--foreground); }\n/* The row's space is padding inside the clipped slot, so its height opens from a true zero. */\n.errorSlot { display: block; overflow: hidden; }\n.error { position: relative; display: block; padding-top: var(--space-2); color: var(--danger); font-size: var(--text-xs); line-height: var(--leading-body); }\n.errorText { display: block; }\n.meterRow { display: flex; align-items: center; gap: var(--space-3); margin-top: var(--space-3); }\n.meter { display: grid; flex: 1 1 auto; min-width: 0; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 5px; }\n/* Each fill slides in from the left inside its clipped track, so the rounded ends keep their shape at every frame. */\n.segment { position: relative; height: 4px; overflow: hidden; isolation: isolate; border-radius: var(--radius-pill); background: var(--border); }\n.fill { position: absolute; inset: 0; border-radius: inherit; background: var(--tone); transition: background-color var(--duration-standard) var(--ease-standard); }\n/* A fixed slot: the meter never resizes when the word changes, and the word stays flush with the field's edge. */\n.word { position: relative; display: block; flex: 0 0 4.5rem; height: 1.4em; color: var(--foreground); font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); text-align: right; white-space: nowrap; }\n.wordText { display: inline-block; }\n.rules { display: grid; gap: 6px; margin: var(--space-4) 0 0; padding: 0; list-style: none; }\n.rule { display: flex; min-height: 1.25rem; align-items: center; gap: 10px; color: var(--text-muted); font-size: var(--text-sm); line-height: var(--leading-body); transition: color var(--duration-standard) var(--ease-standard); }\n.rule[data-met] { color: var(--foreground); }\n.ruleLabel { min-width: 0; }\n/* The outline hands over to a faint tint while the tick draws on top; most of the color sits on the tick. */\n.mark { position: relative; display: block; width: 16px; height: 16px; flex: 0 0 16px; border: 1.25px solid var(--border-strong); border-radius: var(--radius-pill); color: var(--success); transition: border-color var(--duration-fast) var(--ease-standard); }\n.rule[data-met] .mark { border-color: transparent; }\n.markFill { position: absolute; inset: -1.25px; border-radius: inherit; background: color-mix(in oklab, var(--success) 16%, transparent); }\n.tick { position: absolute; inset: -1.25px; width: 16px; height: 16px; }\n.remaining { position: relative; display: block; margin-left: auto; padding-left: var(--space-2); color: var(--text-muted); font-size: var(--text-xs); white-space: nowrap; }\n.remainingText { display: inline-flex; align-items: baseline; gap: .28em; }\n.number { display: inline-flex; font-variant-numeric: tabular-nums; }\n/* Places clip sideways only, so a narrowing column never shows half a digit and a turning digit is never cut. */\n.place { position: relative; display: inline-block; overflow-x: clip; }\n.digit { display: inline-block; }\n.srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }\n@media (prefers-reduced-motion: reduce) {\n  .shell, .reveal, .fill, .rule, .mark { transition: none; }\n  .reveal:active { transform: none; }\n  .input[data-reveal] { animation: none; }\n}\n"
    }
  ],
  "docs": "Docs and live preview: https://ui.xzero.co.uk/components/password-strength",
  "meta": {
    "tier": "free",
    "kind": "component",
    "tags": [
      "password",
      "strength meter",
      "validation"
    ],
    "docs": "https://ui.xzero.co.uk/components/password-strength",
    "markdown": "https://ui.xzero.co.uk/components/password-strength/markdown"
  }
}