{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "password-input",
  "type": "registry:component",
  "description": "A password input with show/hide toggle and an optional strength indicator",
  "dependencies": [
    "@zxcvbn-ts/core",
    "@zxcvbn-ts/language-common",
    "@zxcvbn-ts/language-en",
    "@zxcvbn-ts/language-de",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://kaui-shadcn-registry.vercel.app/r/input-group.json"
  ],
  "files": [
    {
      "type": "registry:component",
      "target": "components/ui/password-input.tsx",
      "path": "src/registry/base/password-input/components/password-input.tsx",
      "content": "\"use client\";\n\nimport { ZxcvbnFactory, type OptionsType } from \"@zxcvbn-ts/core\";\nimport { CircleCheckIcon, CircleIcon, EyeIcon, EyeOffIcon } from \"lucide-react\";\nimport {\n  type ChangeEvent,\n  type ComponentProps,\n  createContext,\n  type ReactNode,\n  useContext,\n  useDeferredValue,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport type { Input } from \"@/components/ui/input\";\nimport {\n  InputGroup,\n  InputGroupAddon,\n  InputGroupButton,\n  InputGroupInput,\n} from \"@/components/ui/input-group\";\nimport { cn } from \"@/lib/utils\";\n\nconst PasswordInputContext = createContext<{ password: string } | null>(null);\n\nexport function PasswordInput({\n  children,\n  onChange,\n  value,\n  defaultValue,\n  ...props\n}: Omit<ComponentProps<typeof Input>, \"type\"> & {\n  children?: ReactNode;\n}) {\n  const [showPassword, setShowPassword] = useState(false);\n  const [password, setPassword] = useState(defaultValue ?? \"\");\n\n  const Icon = showPassword ? EyeOffIcon : EyeIcon;\n  const currentValue = value ?? password;\n\n  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {\n    setPassword(e.target.value);\n    onChange?.(e);\n  };\n\n  return (\n    <PasswordInputContext value={{ password: currentValue.toString() }}>\n      <div className=\"space-y-3\">\n        <InputGroup>\n          <InputGroupInput\n            {...props}\n            value={value}\n            defaultValue={defaultValue}\n            type={showPassword ? \"text\" : \"password\"}\n            onChange={handleChange}\n          />\n          <InputGroupAddon align=\"inline-end\">\n            <InputGroupButton\n              size=\"icon-xs\"\n              onClick={() => setShowPassword((p) => !p)}\n            >\n              <Icon className=\"size-4.5\" />\n              <span className=\"sr-only\">\n                {showPassword ? \"Hide password\" : \"Show password\"}\n              </span>\n            </InputGroupButton>\n          </InputGroupAddon>\n        </InputGroup>\n        {children}\n      </div>\n    </PasswordInputContext>\n  );\n}\n\nexport function PasswordInputStrengthChecker({\n  options,\n  userInputs,\n  threshold = 3,\n  showFeedback = true,\n  onScoreChange,\n}: {\n  options?: OptionsType;\n  userInputs?: (string | number)[];\n  threshold?: 0 | 1 | 2 | 3 | 4;\n  showFeedback?: boolean;\n  onScoreChange?: (score: 0 | 1 | 2 | 3 | 4) => void;\n}) {\n  const [zxcvbn, setZxcvbn] = useState<InstanceType<\n    typeof ZxcvbnFactory\n  > | null>(null);\n  const [errorLoadingOptions, setErrorLoadingOptions] = useState(false);\n  const optionsRef = useRef(options);\n  const onScoreChangeRef = useRef(onScoreChange);\n  onScoreChangeRef.current = onScoreChange;\n\n  const { password } = usePasswordInput();\n  const deferredPassword = useDeferredValue(password);\n\n  const strengthResult = useMemo(() => {\n    if (!zxcvbn || deferredPassword.length === 0) {\n      return {\n        score: 0 as const,\n        feedback: {\n          warning: undefined as string | undefined,\n          suggestions: [] as string[],\n        },\n      };\n    }\n    return zxcvbn.check(deferredPassword, userInputs);\n  }, [zxcvbn, deferredPassword, userInputs]);\n\n  useEffect(() => {\n    const opts = optionsRef.current;\n    if (opts) {\n      if (opts.translations) {\n        setZxcvbn(new ZxcvbnFactory(opts));\n      } else {\n        import(\"@zxcvbn-ts/language-en\")\n          .then((english) => {\n            setZxcvbn(\n              new ZxcvbnFactory({\n                translations: english.translations,\n                ...opts,\n              }),\n            );\n          })\n          .catch(() => setErrorLoadingOptions(true));\n      }\n      return;\n    }\n    Promise.all([\n      import(\"@zxcvbn-ts/language-common\"),\n      import(\"@zxcvbn-ts/language-en\"),\n    ])\n      .then(([common, english]) => {\n        setZxcvbn(\n          new ZxcvbnFactory({\n            translations: english.translations,\n            graphs: common.adjacencyGraphs,\n            dictionary: {\n              ...common.dictionary,\n              ...english.dictionary,\n            },\n          }),\n        );\n      })\n      .catch(() => setErrorLoadingOptions(true));\n  }, []);\n\n  useEffect(() => {\n    onScoreChangeRef.current?.(strengthResult.score);\n  }, [strengthResult.score]);\n\n  function getLabel() {\n    if (deferredPassword.length === 0) return \"Password strength\";\n    if (!zxcvbn) return \"Checking...\";\n    switch (strengthResult.score) {\n      case 0:\n      case 1:\n        return \"Very weak\";\n      case 2:\n        return \"Weak\";\n      case 3:\n        return \"Strong\";\n      case 4:\n        return \"Very strong\";\n      default:\n        return \"Unknown\";\n    }\n  }\n\n  const label = getLabel();\n  const color =\n    strengthResult.score >= threshold ? \"bg-primary\" : \"bg-destructive\";\n  const { warning, suggestions } = strengthResult.feedback;\n  const hasFeedback =\n    showFeedback &&\n    deferredPassword.length > 0 &&\n    (warning != null || suggestions.length > 0);\n\n  if (errorLoadingOptions) return null;\n\n  return (\n    <div className=\"space-y-1.5\">\n      <div\n        role=\"progressbar\"\n        aria-label=\"Password strength\"\n        aria-valuenow={strengthResult.score}\n        aria-valuemin={0}\n        aria-valuemax={4}\n        aria-valuetext={label}\n        className=\"flex gap-1\"\n      >\n        {([0, 1, 2, 3] as const).map((level) => (\n          <div\n            key={`strength-bar-${level}`}\n            className={cn(\n              \"h-1 flex-1 rounded-full transition-colors\",\n              strengthResult.score > level ? color : \"bg-secondary\",\n            )}\n          />\n        ))}\n      </div>\n      <div className=\"flex justify-end text-sm text-muted-foreground\">\n        {label}\n      </div>\n      {hasFeedback && (\n        <div className=\"space-y-0.5 text-sm text-muted-foreground\">\n          {warning && <p>{warning}</p>}\n          {suggestions.map((s, i) => (\n            <p key={i}>{s}</p>\n          ))}\n        </div>\n      )}\n    </div>\n  );\n}\n\nexport type PasswordRule = {\n  label: string;\n  test: (password: string) => boolean;\n};\n\nexport function PasswordInputRules({ rules }: { rules: PasswordRule[] }) {\n  const { password } = usePasswordInput();\n  const idle = password.length === 0;\n\n  return (\n    <ul className=\"space-y-1\">\n      {rules.map((rule, index) => {\n        const passed = !idle && rule.test(password);\n        return (\n          <li\n            key={index}\n            className={cn(\n              \"flex items-center gap-2 text-sm transition-colors\",\n              passed ? \"text-primary\" : \"text-muted-foreground\",\n            )}\n          >\n            {passed ? (\n              <CircleCheckIcon className=\"size-3.5 shrink-0\" />\n            ) : (\n              <CircleIcon className=\"size-3.5 shrink-0\" />\n            )}\n            {rule.label}\n          </li>\n        );\n      })}\n    </ul>\n  );\n}\n\nconst usePasswordInput = () => {\n  const context = useContext(PasswordInputContext);\n  if (context == null) {\n    throw new Error(\n      \"usePasswordInput must be used within a PasswordInputContext\",\n    );\n  }\n  return context;\n};\n"
    }
  ]
}