{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "password-input", "description": "An input that can toggle visibility of the password text and check the strength of the password.", "dependencies": [ "@zxcvbn-ts/core", "@zxcvbn-ts/language-common", "@zxcvbn-ts/language-en" ], "registryDependencies": ["input", "button", "tooltip", "input-group"], "files": [ { "path": "src/registry/new-york/items/password-input/components/password-input.tsx", "content": "\"use client\"\n\nimport { Input } from \"@/components/ui/input\"\nimport { cn } from \"@/lib/utils\"\nimport { EyeIcon, EyeOffIcon } from \"lucide-react\"\nimport {\n useState,\n createContext,\n useContext,\n type ComponentProps,\n type ReactNode,\n type ChangeEvent,\n useEffect,\n useDeferredValue,\n useMemo,\n} from \"react\"\nimport { zxcvbn, zxcvbnOptions } from \"@zxcvbn-ts/core\"\nimport {\n Tooltip,\n TooltipContent,\n TooltipTrigger,\n} from \"@/components/ui/tooltip\"\nimport {\n InputGroup,\n InputGroupAddon,\n InputGroupButton,\n InputGroupInput,\n} from \"@/components/ui/input-group\"\n\nconst PasswordInputContext = createContext<{ password: string } | null>(null)\n\nexport function PasswordInput({\n children,\n onChange,\n value,\n defaultValue,\n ...props\n}: Omit, \"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) => {\n setPassword(e.target.value)\n onChange?.(e)\n }\n\n return (\n \n
\n \n \n \n setShowPassword(p => !p)}\n >\n \n \n {showPassword ? \"Hide password\" : \"Show password\"}\n \n \n \n \n {children}\n
\n
\n )\n}\n\nexport function PasswordInputStrengthChecker() {\n const [optionsLoaded, setOptionsLoaded] = useState(false)\n const [errorLoadingOptions, setErrorLoadingOptions] = useState(false)\n\n const { password } = usePasswordInput()\n const deferredPassword = useDeferredValue(password)\n const strengthResult = useMemo(() => {\n if (!optionsLoaded || deferredPassword.length === 0) {\n return { score: 0, feedback: { warning: undefined } } as const\n }\n\n return zxcvbn(deferredPassword)\n }, [optionsLoaded, deferredPassword])\n\n useEffect(() => {\n Promise.all([\n import(\"@zxcvbn-ts/language-common\"),\n import(\"@zxcvbn-ts/language-en\"),\n ])\n .then(([common, english]) => {\n zxcvbnOptions.setOptions({\n translations: english.translations,\n graphs: common.adjacencyGraphs,\n maxLength: 50,\n dictionary: {\n ...common.dictionary,\n ...english.dictionary,\n },\n })\n setOptionsLoaded(true)\n })\n .catch(() => setErrorLoadingOptions(true))\n }, [])\n\n function getLabel() {\n if (deferredPassword.length === 0) return \"Password strength\"\n if (!optionsLoaded) return \"Loading strength checker\"\n\n const score = strengthResult.score\n switch (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 throw new Error(`Invalid score: ${score satisfies never}`)\n }\n }\n\n const label = getLabel()\n\n if (errorLoadingOptions) return null\n\n return (\n
\n \n {Array.from({ length: 4 }).map((_, i) => {\n const color =\n strengthResult.score >= 3 ? \"bg-primary\" : \"bg-destructive\"\n\n return (\n i ? color : \"bg-secondary\",\n )}\n />\n )\n })}\n
\n
\n {strengthResult.feedback.warning == null ? (\n label\n ) : (\n \n \n {label}\n \n \n {strengthResult.feedback.warning}\n \n \n )}\n
\n \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", "type": "registry:ui" } ], "type": "registry:component" }