{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "password-input",
  "title": "Password Input",
  "description": "A password field with a reveal button and a caps lock hint, plus a strength meter, a live checklist or a meter on the field edge.",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://miseui.junsu4621.workers.dev/r/motion.json"
  ],
  "files": [
    {
      "path": "registry/miseui/ui/password-input.tsx",
      "content": "import type * as React from \"react\"\nimport { useEffect, useRef, useState } from \"react\"\nimport { CapsLockHint, Checklist, EdgeMeter, RevealButton, SegmentMeter, type PasswordLabels } from \"@/registry/miseui/ui/password-input-parts\"\nimport { DEFAULT_MIN_LENGTH, passwordChecks, scorePassword } from \"@/registry/miseui/ui/password-input-strength\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\ntype PasswordInputVariant = \"plain\" | \"strength\" | \"checklist\" | \"inline\"\ntype PasswordInputEffect = \"spring\"\n\nconst defaultLabels = (requiredLength: number): PasswordLabels => ({\n  show: \"Show password\",\n  capsLock: \"Caps Lock is on\",\n  meter: \"Password strength\",\n  scores: [\"Weak\", \"Fair\", \"Good\", \"Strong\"],\n  checks: {\n    length: `At least ${requiredLength} characters`,\n    case: \"Upper and lower case letters\",\n    number: \"A number\",\n    symbol: \"A symbol\",\n  },\n  met: \"Met\",\n  unmet: \"Not met yet\",\n})\n\nconst FIELD_CLASS = [\n  \"h-9 w-full min-w-0 rounded-md border border-input bg-transparent py-1 pl-3 text-base shadow-xs md:text-sm pointer-coarse:h-10\",\n  \"placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground\",\n  \"transition-[color,border-color,box-shadow] duration-(--duration-fast) ease-(--ease-mise)\",\n  \"outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n  \"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40\",\n  \"disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50\",\n]\n\ntype FieldHandlers = Pick<React.ComponentProps<\"input\">, \"onChange\" | \"onKeyDown\" | \"onKeyUp\" | \"onBlur\">\n\n/** Caps Lock can only be read from a key event, so the hint appears on the first key press and leaves with focus. */\nfunction useFieldState(defaultValue: React.ComponentProps<\"input\">[\"defaultValue\"], handlers: FieldHandlers) {\n  const [typed, setTyped] = useState(String(defaultValue ?? \"\"))\n  const [capsLock, setCapsLock] = useState(false)\n  const events: Required<FieldHandlers> = {\n    onChange: (event) => {\n      setTyped(event.target.value)\n      handlers.onChange?.(event)\n    },\n    onKeyDown: (event) => {\n      setCapsLock(event.getModifierState(\"CapsLock\"))\n      handlers.onKeyDown?.(event)\n    },\n    onKeyUp: (event) => {\n      setCapsLock(event.getModifierState(\"CapsLock\"))\n      handlers.onKeyUp?.(event)\n    },\n    onBlur: (event) => {\n      setCapsLock(false)\n      handlers.onBlur?.(event)\n    },\n  }\n  return { typed, capsLock, events }\n}\n\n/** A revealed password would otherwise be offered to the browser's autofill as plain text when the form submits. */\nfunction useReveal() {\n  const inputRef = useRef<HTMLInputElement>(null)\n  const [revealed, setRevealed] = useState(false)\n  useEffect(() => {\n    const form = inputRef.current?.form\n    if (!form) return\n    const hide = () => setRevealed(false)\n    form.addEventListener(\"submit\", hide)\n    return () => form.removeEventListener(\"submit\", hide)\n  }, [])\n  return { inputRef, revealed, toggle: () => setRevealed((current) => !current) }\n}\n\ntype PasswordInputProps = Omit<React.ComponentProps<\"input\">, \"type\"> & {\n  /** `plain` field and reveal, `strength` a meter below, `checklist` requirements ticking off, `inline` the meter on the field edge. */\n  variant?: PasswordInputVariant\n  /** `spring` eases the meter to its new level. */\n  effect?: PasswordInputEffect\n  /** The length the first check and the strength cap ask for. This is not the HTML attribute. */\n  requiredLength?: number\n  /** Replaces any wording, for your language. */\n  labels?: Partial<PasswordLabels>\n}\n\nfunction PasswordInput({ variant = \"plain\", effect, requiredLength = DEFAULT_MIN_LENGTH, labels, className, autoComplete, onChange, onKeyDown, onKeyUp, onBlur, ...props }: PasswordInputProps) {\n  const { inputRef, revealed, toggle } = useReveal()\n  const { typed, capsLock, events } = useFieldState(props.defaultValue, { onChange, onKeyDown, onKeyUp, onBlur })\n  const text = props.value !== undefined ? String(props.value) : typed\n  const score = scorePassword(text, requiredLength)\n  const words = { ...defaultLabels(requiredLength), ...labels }\n  const spring = effect === \"spring\"\n  const inline = variant === \"inline\"\n  return (\n    <div data-slot=\"password-input\" data-variant={variant} data-effect={effect} className={cn(\"grid w-full min-w-0 gap-2\", className)}>\n      <div className=\"relative min-w-0\">\n        <input\n          ref={inputRef}\n          {...props}\n          {...events}\n          data-slot=\"password-input-field\"\n          type={revealed ? \"text\" : \"password\"}\n          // New-password variants tell the manager to offer a generated one instead of filling an old one in.\n          autoComplete={autoComplete ?? (variant === \"plain\" ? \"current-password\" : \"new-password\")}\n          autoCapitalize=\"off\"\n          autoCorrect=\"off\"\n          // Without this, some browsers send a revealed password to a cloud spell checker.\n          spellCheck={false}\n          className={cn(FIELD_CLASS, inline ? \"pr-24\" : \"pr-9 pointer-coarse:pr-10\")}\n        />\n        {inline && score > 0 ? (\n          <span data-slot=\"password-input-score\" className=\"pointer-events-none absolute inset-y-0 right-10 flex items-center text-[13px] text-muted-foreground\">\n            {words.scores[score - 1]}\n          </span>\n        ) : null}\n        <RevealButton revealed={revealed} label={words.show} disabled={props.disabled} onToggle={toggle} />\n        {inline ? <EdgeMeter score={score} labels={words} spring={spring} /> : null}\n      </div>\n      {capsLock ? <CapsLockHint text={words.capsLock} /> : null}\n      {variant === \"strength\" ? <SegmentMeter score={score} labels={words} spring={spring} /> : null}\n      {variant === \"checklist\" ? <Checklist checks={passwordChecks(text, requiredLength)} labels={words} /> : null}\n    </div>\n  )\n}\n\nexport { PasswordInput, passwordChecks, scorePassword, type PasswordInputProps, type PasswordInputVariant, type PasswordInputEffect, type PasswordLabels }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/password-input-parts.tsx",
      "content": "import { Check, Circle, Eye, EyeOff, TriangleAlert } from \"lucide-react\"\nimport { motion, useReducedMotion } from \"motion/react\"\nimport { SPRING_DEFAULT } from \"@/registry/miseui/lib/motion\"\nimport type { PasswordCheck, PasswordCheckId, PasswordScore } from \"@/registry/miseui/ui/password-input-strength\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\nconst INSTANT = { duration: 0 } as const\nconst MAX_SCORE = 4\nconst SEGMENTS = [1, 2, 3, 4] as const\n\n/** Fixed hues: strength is a verdict, and the accent may itself be amber or red. */\nconst SCORE_FILL: Record<PasswordScore, string> = {\n  0: \"bg-transparent\",\n  1: \"bg-destructive\",\n  2: \"bg-amber-500\",\n  3: \"bg-emerald-500\",\n  4: \"bg-emerald-500\",\n}\n\ninterface PasswordLabels {\n  show: string\n  capsLock: string\n  meter: string\n  scores: readonly [string, string, string, string]\n  checks: Record<PasswordCheckId, string>\n  met: string\n  unmet: string\n}\n\ninterface RevealButtonProps {\n  revealed: boolean\n  label: string\n  disabled?: boolean\n  onToggle: () => void\n}\n\nfunction RevealButton({ revealed, label, disabled, onToggle }: RevealButtonProps) {\n  const Icon = revealed ? EyeOff : Eye\n  return (\n    <button\n      type=\"button\"\n      data-slot=\"password-input-reveal\"\n      aria-label={label}\n      aria-pressed={revealed}\n      disabled={disabled}\n      // Keeps the caret where it was in the field when this is clicked.\n      onMouseDown={(event) => event.preventDefault()}\n      onClick={onToggle}\n      className={cn(\n        \"absolute inset-y-0 right-0 flex w-9 items-center justify-center rounded-r-md text-muted-foreground outline-none pointer-coarse:w-10\",\n        \"transition-colors duration-(--duration-fast) ease-(--ease-mise) hover:text-foreground\",\n        \"focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50\",\n      )}\n    >\n      <Icon aria-hidden=\"true\" className=\"size-4\" />\n    </button>\n  )\n}\n\nfunction useMeterTransition(spring: boolean) {\n  const reduceMotion = useReducedMotion() ?? false\n  return spring && !reduceMotion ? SPRING_DEFAULT : INSTANT\n}\n\nconst meterRole = (score: PasswordScore, labels: PasswordLabels) => ({\n  role: \"meter\",\n  \"aria-label\": labels.meter,\n  \"aria-valuemin\": 0,\n  \"aria-valuemax\": MAX_SCORE,\n  \"aria-valuenow\": score,\n  \"aria-valuetext\": score === 0 ? undefined : labels.scores[score - 1],\n})\n\ninterface MeterProps {\n  score: PasswordScore\n  labels: PasswordLabels\n  spring: boolean\n}\n\nfunction SegmentMeter({ score, labels, spring }: MeterProps) {\n  const transition = useMeterTransition(spring)\n  return (\n    <div className=\"flex items-center gap-3\">\n      <div {...meterRole(score, labels)} data-slot=\"password-input-meter\" className=\"grid flex-1 grid-cols-4 gap-1\">\n        {SEGMENTS.map((segment) => (\n          <span key={segment} className=\"h-1 overflow-hidden rounded-full bg-muted\">\n            <motion.span\n              data-slot=\"password-input-segment\"\n              data-filled={score >= segment ? \"true\" : \"false\"}\n              className={cn(\"block h-full origin-left rtl:origin-right\", SCORE_FILL[score])}\n              initial={false}\n              animate={{ scaleX: score >= segment ? 1 : 0 }}\n              transition={transition}\n            />\n          </span>\n        ))}\n      </div>\n      <span data-slot=\"password-input-score\" className=\"min-w-12 text-right text-[13px] text-muted-foreground\">\n        {score === 0 ? null : labels.scores[score - 1]}\n      </span>\n    </div>\n  )\n}\n\n/** One line on the bottom edge of the field, because `inline` has no room below it. */\nfunction EdgeMeter({ score, labels, spring }: MeterProps) {\n  const transition = useMeterTransition(spring)\n  return (\n    <span\n      {...meterRole(score, labels)}\n      data-slot=\"password-input-meter\"\n      className=\"pointer-events-none absolute inset-x-1 bottom-0 h-0.5 overflow-hidden rounded-full\"\n    >\n      <motion.span\n        data-slot=\"password-input-segment\"\n        className={cn(\"block h-full origin-left rtl:origin-right\", SCORE_FILL[score])}\n        initial={false}\n        animate={{ scaleX: score / MAX_SCORE }}\n        transition={transition}\n      />\n    </span>\n  )\n}\n\nfunction Checklist({ checks, labels }: { checks: readonly PasswordCheck[]; labels: PasswordLabels }) {\n  return (\n    <ul data-slot=\"password-input-checklist\" className=\"grid gap-1\">\n      {checks.map((check) => (\n        <li\n          key={check.id}\n          data-met={check.met ? \"true\" : \"false\"}\n          className={cn(\n            \"flex items-center gap-2 text-[13px] leading-5 transition-colors duration-(--duration-fast) ease-(--ease-mise)\",\n            check.met ? \"text-foreground\" : \"text-muted-foreground\",\n          )}\n        >\n          {check.met ? (\n            <Check aria-hidden=\"true\" className=\"size-3.5 shrink-0 text-emerald-600 dark:text-emerald-400\" />\n          ) : (\n            <Circle aria-hidden=\"true\" className=\"size-3.5 shrink-0 opacity-50\" />\n          )}\n          <span className=\"min-w-0\">{labels.checks[check.id]}</span>\n          <span className=\"sr-only\">{check.met ? labels.met : labels.unmet}</span>\n        </li>\n      ))}\n    </ul>\n  )\n}\n\nfunction CapsLockHint({ text }: { text: string }) {\n  return (\n    <p\n      role=\"status\"\n      data-slot=\"password-input-caps\"\n      className=\"flex items-center gap-2 text-[13px] leading-5 text-amber-700 dark:text-amber-400\"\n    >\n      <TriangleAlert aria-hidden=\"true\" className=\"size-3.5 shrink-0\" />\n      {text}\n    </p>\n  )\n}\n\nexport { CapsLockHint, Checklist, EdgeMeter, RevealButton, SegmentMeter, type PasswordLabels }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/password-input-strength.ts",
      "content": "type PasswordScore = 0 | 1 | 2 | 3 | 4\ntype PasswordCheckId = \"length\" | \"case\" | \"number\" | \"symbol\"\n\ninterface PasswordCheck {\n  id: PasswordCheckId\n  met: boolean\n}\n\nconst DEFAULT_MIN_LENGTH = 12\n/** A short password cannot score above fair however varied it looks: length is what slows an attacker most. */\nconst SHORT_PASSWORD_CAP: PasswordScore = 2\nconst WEAK_BELOW_BITS = 30\nconst FAIR_BELOW_BITS = 50\nconst GOOD_BELOW_BITS = 72\nconst LOWER_POOL = 26\nconst UPPER_POOL = 26\nconst DIGIT_POOL = 10\nconst SYMBOL_POOL = 33\n/** Hangul, Cyrillic and the rest: a rough figure, but they do widen the search. */\nconst OTHER_POOL = 100\n\n/** Counts the characters that add information. The rest of a repeat (aaaa) or a run (abcd, 4321) gives an attacker nothing to guess. */\nfunction effectiveLength(password: string) {\n  const codes = [...password].map((character) => character.codePointAt(0) ?? 0)\n  return codes.reduce((count, code, index) => {\n    if (index === 0) return 1\n    const step = code - codes[index - 1]\n    const previousStep = index > 1 ? codes[index - 1] - codes[index - 2] : null\n    const repeats = step === 0\n    const continuesRun = Math.abs(step) === 1 && step === previousStep\n    return repeats || continuesRun ? count : count + 1\n  }, 0)\n}\n\nfunction characterPool(password: string) {\n  const pools = [\n    [/[a-z]/, LOWER_POOL],\n    [/[A-Z]/, UPPER_POOL],\n    [/[0-9]/, DIGIT_POOL],\n    [/[\\x20-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\x7e]/, SYMBOL_POOL],\n    [/[^\\x20-\\x7e]/, OTHER_POOL],\n  ] as const\n  return pools.reduce((total, [pattern, size]) => (pattern.test(password) ? total + size : total), 0)\n}\n\nfunction estimateBits(password: string) {\n  if (!password) return 0\n  return effectiveLength(password) * Math.log2(characterPool(password))\n}\n\n/** A rough estimate with no dictionary, so a long common phrase scores higher than it should. It guides; the server decides. */\nfunction scorePassword(password: string, minLength = DEFAULT_MIN_LENGTH): PasswordScore {\n  if (!password) return 0\n  const bits = estimateBits(password)\n  const score: PasswordScore = bits < WEAK_BELOW_BITS ? 1 : bits < FAIR_BELOW_BITS ? 2 : bits < GOOD_BELOW_BITS ? 3 : 4\n  if ([...password].length >= minLength) return score\n  return Math.min(score, SHORT_PASSWORD_CAP) as PasswordScore\n}\n\nfunction passwordChecks(password: string, minLength = DEFAULT_MIN_LENGTH): readonly PasswordCheck[] {\n  return [\n    { id: \"length\", met: [...password].length >= minLength },\n    { id: \"case\", met: /[a-z]/.test(password) && /[A-Z]/.test(password) },\n    { id: \"number\", met: /[0-9]/.test(password) },\n    { id: \"symbol\", met: /[^a-zA-Z0-9]/.test(password) },\n  ]\n}\n\nexport {\n  DEFAULT_MIN_LENGTH,\n  effectiveLength,\n  estimateBits,\n  passwordChecks,\n  scorePassword,\n  type PasswordCheck,\n  type PasswordCheckId,\n  type PasswordScore,\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "forms"
  ],
  "type": "registry:ui"
}