{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "password-strength",
  "type": "registry:component",
  "title": "Password Strength",
  "description": "A password field that explains itself: it spots common words, years and keyboard runs, says how long a crack would take, and ticks off what's left in plain words.",
  "categories": [
    "component",
    "Forms"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/devl/password-strength.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { EyeIcon, EyeOffIcon, TimerIcon } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useId, useMemo, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst COMMON = [\n  \"password\", \"qwerty\", \"letmein\", \"welcome\", \"admin\", \"iloveyou\", \"monkey\", \"dragon\", \"football\", \"baseball\",\n  \"sunshine\", \"princess\", \"summer\", \"winter\", \"autumn\", \"spring\", \"shadow\", \"master\", \"hello\", \"freedom\",\n  \"whatever\", \"trustno1\", \"starwars\", \"login\", \"secret\", \"superman\", \"batman\", \"michael\", \"jordan\", \"charlie\",\n  \"computer\", \"internet\", \"cookie\", \"pepper\", \"ginger\", \"orange\", \"banana\", \"chelsea\", \"liverpool\", \"arsenal\",\n  \"soccer\", \"hockey\", \"flower\", \"purple\", \"silver\", \"golden\", \"abc123\", \"123456\", \"passw0rd\", \"changeme\",\n];\nconst RUNS = [\"qwertyuiop\", \"asdfghjkl\", \"zxcvbnm\", \"abcdefghijklmnopqrstuvwxyz\", \"01234567890\"];\nconst LEET: Record<string, string> = { \"0\": \"o\", \"1\": \"i\", \"3\": \"e\", \"4\": \"a\", \"5\": \"s\", \"7\": \"t\", \"@\": \"a\", $: \"s\", \"!\": \"i\" };\n\nexport interface Analysis {\n  score: 0 | 1 | 2 | 3 | 4;\n  bits: number;\n  reason: string;\n  crackTime: string;\n  checks: { id: string; label: string; met: boolean }[];\n}\n\nconst OFFLINE_GUESSES_PER_SECOND = 1e10;\n\nconst humanTime = (seconds: number) => {\n  if (seconds < 1) return \"instantly\";\n  const units: [number, string][] = [\n    [60, \"second\"],\n    [60, \"minute\"],\n    [24, \"hour\"],\n    [30, \"day\"],\n    [12, \"month\"],\n    [100, \"year\"],\n  ];\n  let v = seconds;\n  for (const [size, name] of units) {\n    if (v < size) return `about ${Math.max(1, Math.round(v))} ${name}${Math.round(v) === 1 ? \"\" : \"s\"}`;\n    v /= size;\n  }\n  return v < 1000 ? `about ${Math.round(v)} centuries` : \"forever\";\n};\n\nexport function analysePassword(pw: string, minLength = 12): Analysis {\n  const lower = /[a-z]/.test(pw);\n  const upper = /[A-Z]/.test(pw);\n  const digit = /\\d/.test(pw);\n  const symbol = /[^a-zA-Z0-9]/.test(pw);\n  const pool = (lower ? 26 : 0) + (upper ? 26 : 0) + (digit ? 10 : 0) + (symbol ? 33 : 0) || 1;\n  const perChar = Math.log2(pool);\n  let bits = pw.length * perChar;\n  const found: string[] = [];\n\n  const plain = pw.toLowerCase().replace(/[013457@$!]/g, (c) => LEET[c] ?? c);\n  const lowerPw = pw.toLowerCase();\n  const exact = COMMON.includes(plain) || COMMON.includes(lowerPw);\n  let word = exact ? undefined : COMMON.filter((w) => w.length >= 4 && plain.includes(w)).sort((a, b) => b.length - a.length)[0];\n  let run = exact\n    ? undefined\n    : RUNS.flatMap((r) => [10, 9, 8, 7, 6, 5, 4].flatMap((len) => [...Array(Math.max(r.length - len + 1, 0))].map((_, i) => r.slice(i, i + len))))\n        .sort((a, b) => b.length - a.length)\n        .find((r) => lowerPw.includes(r));\n  if (word && run) {\n    const w = plain.indexOf(word);\n    const r = lowerPw.indexOf(run);\n    if (w < r + run.length && r < w + word.length) {\n      if (run.length > word.length) word = undefined;\n      else run = undefined;\n    }\n  }\n  if (word) {\n    bits -= word.length * perChar - 12;\n    found.push(`it's built on the common word “${word}”`);\n  }\n  const year = pw.match(/(19|20)\\d{2}/);\n  if (year) {\n    bits -= 4 * perChar - 7;\n    found.push(`it has a year in it (${year[0]})`);\n  }\n  if (run) {\n    bits -= run.length * perChar - 5;\n    found.push(`“${run}” is a keyboard run`);\n  }\n  const repeat = pw.match(/(.)\\1{2,}/);\n  if (repeat) {\n    bits -= (repeat[0].length - 1) * perChar;\n    found.push(`it repeats “${repeat[1]}”`);\n  }\n  if (exact) bits = Math.min(bits, 8);\n  bits = Math.max(0, bits);\n\n  const score = (bits < 28 ? 0 : bits < 40 ? 1 : bits < 60 ? 2 : bits < 80 ? 3 : 4) as Analysis[\"score\"];\n  const checks = [\n    { id: \"length\", label: `${minLength}+ characters`, met: pw.length >= minLength },\n    { id: \"case\", label: \"Upper & lower case\", met: lower && upper },\n    { id: \"mix\", label: \"Number or symbol\", met: digit || symbol },\n    { id: \"pattern\", label: \"No common patterns\", met: pw.length > 0 && !exact && found.length === 0 },\n  ];\n\n  let reason: string;\n  if (!pw) reason = `Use ${minLength} or more characters. A short, odd phrase is easier to remember than symbols.`;\n  else if (exact) reason = \"That's one of the first passwords anyone would try.\";\n  else {\n    const size =\n      pw.length < 8\n        ? `${pw.length} characters is very short`\n        : pw.length < minLength\n          ? `${pw.length} characters is on the short side`\n          : pw.length >= 20\n            ? \"Lovely and long\"\n            : \"Good length\";\n    if (found.length) {\n      const joiner = pw.length >= minLength ? \", but \" : \", and \";\n      reason = `${size}${joiner}${found.slice(0, 2).join(\" and \")}.`;\n    } else if (score >= 3) {\n      reason = `${size}, with no patterns an attacker would try first.`;\n    } else {\n      const missing = !upper || !lower ? \"mixing upper and lower case\" : !digit && !symbol ? \"a number or symbol\" : \"a few more characters\";\n      reason = `${size}. ${pw.length < minLength ? \"A few more characters\" : `Try ${missing}`} would help most.`;\n    }\n  }\n\n  const crackTime = humanTime(2 ** bits / 2 / OFFLINE_GUESSES_PER_SECOND);\n  return { score, bits, reason, crackTime, checks };\n}\n\nconst LEVELS = [\n  { label: \"Very weak\", bar: \"bg-destructive\", text: \"text-destructive-foreground\" },\n  { label: \"Weak\", bar: \"bg-destructive\", text: \"text-destructive-foreground\" },\n  { label: \"Fair\", bar: \"bg-warning\", text: \"text-warning-foreground\" },\n  { label: \"Good\", bar: \"bg-success\", text: \"text-success-foreground\" },\n  { label: \"Strong\", bar: \"bg-success\", text: \"text-success-foreground\" },\n];\n\nexport interface PasswordStrengthProps {\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string, analysis: Analysis) => void;\n  label?: string;\n  minLength?: number;\n  name?: string;\n  autoComplete?: string;\n  className?: string;\n}\n\nexport function PasswordStrength({\n  value: valueProp,\n  defaultValue = \"\",\n  onValueChange,\n  label = \"Password\",\n  minLength = 12,\n  name,\n  autoComplete = \"new-password\",\n  className,\n}: PasswordStrengthProps) {\n  const [inner, setInner] = useState(defaultValue);\n  const value = valueProp ?? inner;\n  const [visible, setVisible] = useState(false);\n  const reduce = useReducedMotion();\n  const id = useId();\n  const analysis = useMemo(() => analysePassword(value, minLength), [value, minLength]);\n  const level = LEVELS[analysis.score];\n  const fill = value ? Math.min(Math.max(analysis.bits / 90, 0.06), 1) : 0;\n\n  return (\n    <div className={cn(\"grid w-full gap-2\", className)}>\n      <div className=\"flex items-baseline justify-between\">\n        <label className=\"font-medium text-sm\" htmlFor={id}>\n          {label}\n        </label>\n        <span aria-live=\"polite\" className={cn(\"font-medium text-xs transition-colors duration-200\", value ? level.text : \"text-muted-foreground\")}>\n          {value ? level.label : `${minLength}+ characters`}\n        </span>\n      </div>\n      <div className=\"relative flex rounded-lg border border-input bg-background not-dark:bg-clip-padding shadow-xs/5 ring-ring/24 transition-shadow has-focus-visible:border-ring has-focus-visible:ring-[3px] dark:bg-input/32\">\n        <input\n          aria-describedby={`${id}-reason`}\n          autoComplete={autoComplete}\n          className=\"h-9 w-0 min-w-0 flex-1 bg-transparent pr-1 pl-3 text-base outline-none placeholder:text-muted-foreground/72 sm:text-sm\"\n          id={id}\n          name={name}\n          onChange={(event) => {\n            const next = event.target.value;\n            if (valueProp === undefined) setInner(next);\n            onValueChange?.(next, analysePassword(next, minLength));\n          }}\n          placeholder=\"Something only you would think of\"\n          spellCheck={false}\n          type={visible ? \"text\" : \"password\"}\n          value={value}\n        />\n        <button\n          aria-label={visible ? \"Hide password\" : \"Show password\"}\n          aria-pressed={visible}\n          className=\"mr-0.5 flex w-9 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n          onClick={() => setVisible((v) => !v)}\n          type=\"button\"\n        >\n          {visible ? <EyeOffIcon className=\"size-4\" /> : <EyeIcon className=\"size-4\" />}\n        </button>\n      </div>\n\n      <div className=\"relative h-1.5 overflow-hidden rounded-full bg-input\">\n        <motion.div\n          animate={{ scaleX: fill }}\n          className={cn(\"absolute inset-0 origin-left rounded-full transition-colors duration-300\", level.bar)}\n          initial={false}\n          transition={reduce ? { duration: 0 } : { type: \"spring\", stiffness: 260, damping: 30 }}\n        />\n      </div>\n\n      <div className=\"grid min-h-17 content-start gap-1\" id={`${id}-reason`}>\n        <p className={cn(\"text-pretty text-xs\", value ? \"text-foreground\" : \"text-muted-foreground\")}>{analysis.reason}</p>\n        {value && (\n          <p className=\"flex items-center gap-1 text-muted-foreground text-xs\">\n            <TimerIcon className=\"size-3.5 shrink-0\" />\n            <span className=\"truncate tabular-nums\">\n              {analysis.crackTime === \"instantly\"\n                ? \"Cracked instantly\"\n                : analysis.crackTime === \"forever\"\n                  ? \"Would outlast the sun to crack\"\n                  : `Takes ${analysis.crackTime} to crack`}\n            </span>\n          </p>\n        )}\n      </div>\n\n      <ul className=\"grid grid-cols-2 gap-x-3 gap-y-1.5 border-t pt-3\">\n        {analysis.checks.map((check) => (\n          <li\n            className={cn(\"flex items-center gap-1.5 text-xs transition-colors duration-200\", check.met ? \"text-foreground\" : \"text-muted-foreground\")}\n            key={check.id}\n          >\n            <Tick met={check.met} reduce={!!reduce} />\n            <span>{check.label}</span>\n            <span className=\"sr-only\">{check.met ? \"(done)\" : \"(not yet)\"}</span>\n          </li>\n        ))}\n      </ul>\n    </div>\n  );\n}\n\nfunction Tick({ met, reduce }: { met: boolean; reduce: boolean }) {\n  return (\n    <svg aria-hidden className=\"size-4 shrink-0\" viewBox=\"0 0 16 16\">\n      <motion.circle\n        animate={{ scale: met ? 1 : 0.4, opacity: met ? 1 : 0 }}\n        className=\"fill-success\"\n        cx={8}\n        cy={8}\n        initial={false}\n        r={7}\n        style={{ originX: \"50%\", originY: \"50%\" }}\n        transition={reduce ? { duration: 0 } : { type: \"spring\", stiffness: 500, damping: 28 }}\n      />\n      <circle className={cn(\"fill-none stroke-muted-foreground/40 transition-opacity\", met && \"opacity-0\")} cx={8} cy={8} r={6.25} strokeWidth={1.5} />\n      <motion.path\n        animate={{ pathLength: met ? 1 : 0 }}\n        d=\"M5 8.2 7.1 10.3 11 6.1\"\n        fill=\"none\"\n        initial={false}\n        stroke=\"white\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        strokeWidth={1.75}\n        transition={reduce ? { duration: 0 } : { duration: 0.2, delay: met ? 0.08 : 0, ease: [0.23, 1, 0.32, 1] }}\n      />\n    </svg>\n  );\n}\n"
    }
  ]
}