{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"password-input","type":"registry:ui","title":"Password Input","description":"Password field with an eye toggle that blinks and draws its slash, a four-segment strength meter that springs in, and an optional requirements checklist.","author":"GetUrUI <hello@geturui.dev>","dependencies":["motion","lucide-react"],"registryDependencies":["@geturui/input"],"files":[{"path":"registry/geturui/ui/password-input.tsx","type":"registry:ui","target":"components/ui/password-input.tsx","content":"\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { Check, Lock } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { Input, type InputProps } from \"@/components/ui/input\";\n\nexport type PasswordStrength = 0 | 1 | 2 | 3 | 4;\n\nexport interface PasswordRequirement {\n  label: string;\n  test: (value: string) => boolean;\n}\n\nexport interface PasswordInputProps extends Omit<InputProps, \"type\" | \"trailing\" | \"clearable\" | \"onClear\"> {\n  /** Show the four-segment strength meter under the field. @default true */\n  showStrength?: boolean;\n  /** Custom scorer returning 0 (empty) to 4 (strong). Defaults to a length + character-mix heuristic. */\n  scorer?: (value: string) => PasswordStrength;\n  /** Checklist rendered under the meter; each row ticks when its test passes. */\n  requirements?: PasswordRequirement[];\n  /** Start with the password visible. */\n  defaultVisible?: boolean;\n  /** Called when the show/hide toggle flips. */\n  onVisibleChange?: (visible: boolean) => void;\n}\n\n/** Default scorer: length (8, 12), mixed case, a digit and a symbol, capped at 4. */\nexport function scorePassword(value: string): PasswordStrength {\n  if (!value) return 0;\n  let score = 0;\n  if (value.length >= 8) score++;\n  if (value.length >= 12) score++;\n  if (/[a-z]/.test(value) && /[A-Z]/.test(value)) score++;\n  if (/\\d/.test(value)) score++;\n  if (/[^A-Za-z0-9]/.test(value)) score++;\n  if (value.length < 8) score = Math.min(score, 1);\n  return Math.max(1, Math.min(4, score)) as PasswordStrength;\n}\n\nconst levels: { word: string; bar: string; text: string }[] = [\n  { word: \"\", bar: \"bg-border\", text: \"text-ink-subtle\" },\n  { word: \"Weak\", bar: \"bg-danger\", text: \"text-danger\" },\n  { word: \"Fair\", bar: \"bg-warning\", text: \"text-warning\" },\n  { word: \"Good\", bar: \"bg-info\", text: \"text-info\" },\n  { word: \"Strong\", bar: \"bg-success\", text: \"text-success\" },\n];\n\n/** Eye that blinks on toggle; a slash draws across it while the password is visible. */\nfunction EyeGlyph({ visible, blink }: { visible: boolean; blink: number }) {\n  const reduce = useReducedMotion();\n  const blinking = !reduce && blink > 0;\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2} strokeLinecap=\"round\" strokeLinejoin=\"round\" className=\"size-4\" aria-hidden>\n      <motion.path\n        d=\"M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7S2 12 2 12Z\"\n        animate={blinking ? { scaleY: [1, 0.15, 1] } : undefined}\n        key={`lid-${blink}`}\n        transition={{ duration: 0.28, ease: \"easeInOut\" }}\n        style={{ transformOrigin: \"12px 12px\" }}\n      />\n      <motion.circle\n        cx={12}\n        cy={12}\n        r={3}\n        animate={blinking ? { scaleY: [1, 0, 1] } : undefined}\n        key={`pupil-${blink}`}\n        transition={{ duration: 0.28, ease: \"easeInOut\" }}\n        style={{ transformOrigin: \"12px 12px\" }}\n      />\n      <motion.path\n        d=\"M4 4l16 16\"\n        initial={false}\n        animate={{ pathLength: visible ? 1 : 0, opacity: visible ? 1 : 0 }}\n        transition={reduce ? { duration: 0 } : { type: \"spring\", stiffness: 380, damping: 30 }}\n      />\n    </svg>\n  );\n}\n\n/** Password field with an eye toggle that blinks, a strength meter whose segments spring in, and an optional checklist. */\nexport const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(function PasswordInput(\n  {\n    showStrength = true,\n    scorer = scorePassword,\n    requirements,\n    defaultVisible = false,\n    onVisibleChange,\n    value,\n    defaultValue,\n    onChange,\n    wrapperClassName,\n    disabled,\n    icon,\n    iconLeading,\n    ...props\n  },\n  ref,\n) {\n  const reduce = useReducedMotion();\n  const uid = React.useId();\n  const inputId = props.id ?? `pw${uid.replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n  const [visible, setVisible] = React.useState(defaultVisible);\n  const [blink, setBlink] = React.useState(0);\n  const [inner, setInner] = React.useState(String(defaultValue ?? \"\"));\n  const current = value !== undefined && value !== null ? String(value) : inner;\n  const score = scorer(current);\n  const level = levels[score];\n\n  const toggle = (\n    <button\n      type=\"button\"\n      aria-label=\"Show password\"\n      aria-pressed={visible}\n      aria-controls={inputId}\n      disabled={disabled}\n      onClick={() => {\n        setVisible((v) => !v);\n        setBlink((b) => b + 1);\n        onVisibleChange?.(!visible);\n      }}\n      className=\"-mr-1 grid size-7 shrink-0 place-items-center rounded-[8px] text-ink-subtle outline-none transition-colors hover:bg-surface-hover hover:text-ink focus-visible:shadow-[var(--focus-ring)]\"\n    >\n      <EyeGlyph visible={visible} blink={blink} />\n    </button>\n  );\n\n  return (\n    <div className={cn(\"grid gap-2\", wrapperClassName)}>\n      <Input\n        ref={ref}\n        {...props}\n        id={inputId}\n        type={visible ? \"text\" : \"password\"}\n        autoComplete={props.autoComplete ?? \"current-password\"}\n        spellCheck={false}\n        autoCapitalize=\"off\"\n        disabled={disabled}\n        value={value}\n        defaultValue={value === undefined ? defaultValue : undefined}\n        onChange={(e) => {\n          setInner(e.target.value);\n          onChange?.(e);\n        }}\n        icon={icon}\n        iconLeading={iconLeading ?? (icon ? undefined : Lock)}\n        trailing={toggle}\n        inputClassName={cn(!visible && current && \"tracking-[0.18em]\", props.inputClassName)}\n      />\n\n      {showStrength && (\n        <div className=\"grid gap-1.5\">\n          <div className=\"flex items-center gap-3\">\n            <div\n              role=\"meter\"\n              aria-label=\"Password strength\"\n              aria-valuemin={0}\n              aria-valuemax={4}\n              aria-valuenow={score}\n              aria-valuetext={level.word || \"Empty\"}\n              className=\"grid flex-1 grid-cols-4 gap-1\"\n            >\n              {[1, 2, 3, 4].map((i) => (\n                <span key={i} className=\"relative h-1 overflow-hidden rounded-full bg-surface-sunken\">\n                  <motion.span\n                    className={cn(\"absolute inset-0 origin-left rounded-full\", levels[score].bar)}\n                    initial={false}\n                    animate={{ scaleX: score >= i ? 1 : 0 }}\n                    transition={\n                      reduce\n                        ? { duration: 0 }\n                        : { type: \"spring\", stiffness: 420, damping: 22, delay: score >= i ? (i - 1) * 0.04 : 0 }\n                    }\n                  />\n                </span>\n              ))}\n            </div>\n            <span className=\"relative h-4 w-12 text-right text-xs font-medium\" aria-live=\"polite\">\n              <AnimatePresence mode=\"popLayout\" initial={false}>\n                <motion.span\n                  key={score}\n                  initial={reduce ? { opacity: 0 } : { opacity: 0, filter: \"blur(4px)\", y: 4 }}\n                  animate={{ opacity: 1, filter: \"blur(0px)\", y: 0 }}\n                  exit={reduce ? { opacity: 0 } : { opacity: 0, filter: \"blur(4px)\", y: -4 }}\n                  transition={{ duration: 0.18 }}\n                  className={cn(\"absolute inset-0\", level.text)}\n                >\n                  {level.word}\n                </motion.span>\n              </AnimatePresence>\n            </span>\n          </div>\n        </div>\n      )}\n\n      {requirements && requirements.length > 0 && (\n        <ul className=\"grid gap-1 sm:grid-cols-2\" aria-label=\"Password requirements\">\n          {requirements.map((r) => {\n            const ok = r.test(current);\n            return (\n              <li key={r.label} className={cn(\"flex items-center gap-1.5 text-xs transition-colors duration-200\", ok ? \"text-ink\" : \"text-ink-subtle\")}>\n                <span\n                  className={cn(\n                    \"grid size-3.5 place-items-center rounded-full border transition-colors duration-200\",\n                    ok ? \"border-transparent bg-success-soft text-success\" : \"border-border-strong\",\n                  )}\n                >\n                  <AnimatePresence initial={false}>\n                    {ok && (\n                      <motion.span\n                        initial={reduce ? { opacity: 0 } : { scale: 0, opacity: 0 }}\n                        animate={{ scale: 1, opacity: 1 }}\n                        exit={reduce ? { opacity: 0 } : { scale: 0, opacity: 0 }}\n                        transition={{ type: \"spring\", stiffness: 600, damping: 24 }}\n                        className=\"grid place-items-center\"\n                      >\n                        <Check className=\"size-2.5\" strokeWidth={3} aria-hidden />\n                      </motion.span>\n                    )}\n                  </AnimatePresence>\n                </span>\n                {r.label}\n                <span className=\"sr-only\">{ok ? \"(met)\" : \"(not met)\"}</span>\n              </li>\n            );\n          })}\n        </ul>\n      )}\n    </div>\n  );\n});\n"},{"path":"LICENSE-geturui.md","type":"registry:file","target":"LICENSE-geturui.md","content":"MIT License\n\nCopyright (c) 2026 geturui.com\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"}],"categories":["components","password","input","form","strength meter","auth","sign up"],"meta":{"tier":"free","license":"MIT","docs":"https://geturui.com/docs/password-input"}}