{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"password-input","type":"registry:component","title":"Password Input","description":"Password field with an eye icon that morphs to eye-off, a caps-lock warning, a segmented strength meter whose bars spring-fill and recolor with a sliding Weak/Fair/Good/Strong label from a length-weighted NIST-style heuristic, an optional requirement checklist whose items check off with a draw-in tick, and onStrengthChange.","author":"kara <ztzyrant@gmail.com>","dependencies":["clsx","lucide-react","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/password-input.tsx","type":"registry:component","target":"@components/motion/password-input.tsx","content":"\"use client\";\n// karaui.kamisama-v0.my.id/components/motion/password-input\n\nimport { Lock } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  forwardRef,\n  type KeyboardEvent,\n  type ReactNode,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { Input, type InputProps } from \"@/components/motion/input\";\nimport { EASE_OUT, SPRING_LAYOUT, SPRING_SWAP } from \"@/lib/ease\";\nimport {\n  MIN_PASSWORD_LENGTH,\n  type PasswordStrength,\n  passwordStrength,\n  STRENGTH_COLORS,\n  STRENGTH_LABELS,\n} from \"@/lib/password-strength\";\nimport { cn } from \"@/lib/utils\";\n\nexport type { PasswordStrength } from \"@/lib/password-strength\";\n\nexport type PasswordRequirement = {\n  id: string;\n  label: string;\n  test: (password: string) => boolean;\n};\n\n/** Default checklist: length, number, symbol, mixed case. */\nexport function defaultPasswordRequirements(\n  minLength: number = MIN_PASSWORD_LENGTH,\n): PasswordRequirement[] {\n  return [\n    {\n      id: \"length\",\n      label: `At least ${minLength} characters`,\n      test: (password) => password.length >= minLength,\n    },\n    { id: \"number\", label: \"A number\", test: (password) => /\\d/.test(password) },\n    {\n      id: \"symbol\",\n      label: \"A symbol\",\n      test: (password) => /[^A-Za-z0-9\\s]/.test(password),\n    },\n    {\n      id: \"case\",\n      label: \"Upper and lowercase letters\",\n      test: (password) => /[a-z]/.test(password) && /[A-Z]/.test(password),\n    },\n  ];\n}\n\nconst STRENGTH_TEXT = [\n  \"text-destructive\",\n  \"text-destructive\",\n  \"text-amber-600 dark:text-amber-400\",\n  \"text-amber-600 dark:text-amber-400\",\n  \"text-(--color-success)\",\n] as const;\n\nexport interface PasswordInputProps\n  extends Omit<InputProps, \"type\" | \"leftIcon\" | \"rightIcon\" | \"success\"> {\n  /** Show the segmented strength meter once something is typed. Default true. */\n  strengthMeter?: boolean;\n  /**\n   * Requirement checklist under the field. `true` uses\n   * `defaultPasswordRequirements(minLength)`; pass an array for custom rules.\n   */\n  requirements?: boolean | PasswordRequirement[];\n  /** Length below which the score is 0 (\"Too short\"). Default 8. */\n  minLength?: number;\n  /** Fires whenever the 0-4 score changes. */\n  onStrengthChange?: (strength: PasswordStrength, label: string) => void;\n  /** Allow revealing the password. Default true. */\n  revealable?: boolean;\n  /** Left icon. Defaults to a lock; pass `null` for none. */\n  icon?: ReactNode;\n}\n\n/**\n * Password field over `Input`: eye/eye-off morphing reveal toggle, a caps-lock\n * warning, a spring-filled strength meter driven by the shared\n * `passwordStrength` heuristic, and an optional requirement checklist.\n */\nexport const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>(\n  function PasswordInput(\n    {\n      value: valueProp,\n      defaultValue,\n      onChange,\n      onKeyDown,\n      onKeyUp,\n      onBlur,\n      strengthMeter = true,\n      requirements = false,\n      minLength = MIN_PASSWORD_LENGTH,\n      onStrengthChange,\n      revealable = true,\n      icon,\n      disabled,\n      autoComplete,\n      placeholder = \"Enter a password\",\n      className,\n      ...rest\n    },\n    ref,\n  ) {\n    const reduce = useReducedMotion();\n    const baseId = useId();\n    const statusId = `${baseId}-status`;\n\n    const controlled = valueProp !== undefined;\n    const [internal, setInternal] = useState(defaultValue ?? \"\");\n    const value = controlled ? valueProp : internal;\n\n    const [revealed, setRevealed] = useState(false);\n    const [capsLock, setCapsLock] = useState(false);\n\n    const strength = passwordStrength(value, minLength);\n    const reported = useRef<number | null>(null);\n    const strengthCallback = useRef(onStrengthChange);\n    strengthCallback.current = onStrengthChange;\n    useEffect(() => {\n      if (reported.current === strength) return;\n      reported.current = strength;\n      strengthCallback.current?.(strength, STRENGTH_LABELS[strength]);\n    }, [strength]);\n\n    const rules = useMemo(\n      () =>\n        requirements === true\n          ? defaultPasswordRequirements(minLength)\n          : requirements || [],\n      [requirements, minLength],\n    );\n\n    const readCaps = (event: KeyboardEvent<HTMLInputElement>) => {\n      // getModifierState is missing on some synthetic/IME events.\n      if (typeof event.getModifierState !== \"function\") return;\n      setCapsLock(event.getModifierState(\"CapsLock\"));\n    };\n\n    const showMeter = strengthMeter && value.length > 0;\n\n    return (\n      <div className={cn(\"flex w-full flex-col gap-2\", className)}>\n        <Input\n          {...rest}\n          ref={ref}\n          type={revealed ? \"text\" : \"password\"}\n          value={value}\n          disabled={disabled}\n          placeholder={placeholder}\n          autoComplete={\n            autoComplete ?? (strengthMeter ? \"new-password\" : \"current-password\")\n          }\n          spellCheck={false}\n          aria-describedby={showMeter || capsLock ? statusId : undefined}\n          leftIcon={icon === undefined ? <Lock /> : icon}\n          onChange={(next) => {\n            if (!controlled) setInternal(next);\n            onChange?.(next);\n          }}\n          onKeyDown={(event) => {\n            readCaps(event);\n            onKeyDown?.(event);\n          }}\n          onKeyUp={(event) => {\n            readCaps(event);\n            onKeyUp?.(event);\n          }}\n          onBlur={(event) => {\n            setCapsLock(false);\n            onBlur?.(event);\n          }}\n          rightIcon={\n            revealable ? (\n              <button\n                type=\"button\"\n                disabled={disabled}\n                onClick={() => setRevealed((prev) => !prev)}\n                aria-label={revealed ? \"Hide password\" : \"Show password\"}\n                aria-pressed={revealed}\n                className=\"text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:text-foreground\"\n              >\n                <EyeMorph closed={revealed} reduce={Boolean(reduce)} />\n              </button>\n            ) : undefined\n          }\n        />\n\n        <div id={statusId} className=\"flex flex-col gap-2 px-1\">\n          <AnimatePresence initial={false}>\n            {capsLock ? (\n              <motion.p\n                key=\"caps\"\n                role=\"status\"\n                initial={reduce ? { opacity: 0 } : { opacity: 0, y: -4 }}\n                animate={{ opacity: 1, y: 0 }}\n                exit={\n                  reduce\n                    ? { opacity: 0, transition: { duration: 0.1 } }\n                    : { opacity: 0, y: -4, transition: { duration: 0.12 } }\n                }\n                transition={{ duration: 0.18, ease: EASE_OUT }}\n                className=\"flex items-center gap-1.5 text-xs text-amber-600 dark:text-amber-400\"\n              >\n                <CapsIcon />\n                Caps Lock is on\n              </motion.p>\n            ) : null}\n          </AnimatePresence>\n\n          <AnimatePresence initial={false}>\n            {showMeter ? (\n              <motion.div\n                key=\"meter\"\n                initial={reduce ? { opacity: 0 } : { opacity: 0, y: -4 }}\n                animate={{ opacity: 1, y: 0 }}\n                exit={\n                  reduce\n                    ? { opacity: 0, transition: { duration: 0.1 } }\n                    : { opacity: 0, y: -4, transition: { duration: 0.12 } }\n                }\n                transition={{ duration: 0.18, ease: EASE_OUT }}\n                className=\"flex items-center gap-3\"\n              >\n                <div className=\"flex flex-1 gap-1.5\" aria-hidden>\n                  {[0, 1, 2, 3].map((index) => (\n                    <span\n                      key={index}\n                      className=\"h-1.5 flex-1 overflow-hidden rounded-full bg-muted-foreground/20\"\n                    >\n                      {/* scaleX, not width — transform only, off layout. */}\n                      <motion.span\n                        initial={false}\n                        animate={{ scaleX: index < strength ? 1 : 0 }}\n                        transition={\n                          reduce\n                            ? { duration: 0 }\n                            : { ...SPRING_LAYOUT, delay: index * 0.03 }\n                        }\n                        className={cn(\n                          \"block h-full w-full origin-left rounded-full transition-colors duration-300\",\n                          STRENGTH_COLORS[strength],\n                        )}\n                      />\n                    </span>\n                  ))}\n                </div>\n                <span\n                  aria-live=\"polite\"\n                  className=\"relative grid min-w-16 justify-items-end overflow-hidden text-xs font-medium\"\n                >\n                  <span className=\"sr-only\">Password strength: </span>\n                  <AnimatePresence initial={false} mode=\"popLayout\">\n                    <motion.span\n                      key={strength}\n                      initial={reduce ? { opacity: 0 } : { opacity: 0, y: 8 }}\n                      animate={{ opacity: 1, y: 0 }}\n                      exit={\n                        reduce\n                          ? { opacity: 0, transition: { duration: 0.08 } }\n                          : { opacity: 0, y: -8, transition: { duration: 0.1 } }\n                      }\n                      transition={reduce ? { duration: 0.12 } : SPRING_SWAP}\n                      className={STRENGTH_TEXT[strength]}\n                    >\n                      {STRENGTH_LABELS[strength]}\n                    </motion.span>\n                  </AnimatePresence>\n                </span>\n              </motion.div>\n            ) : null}\n          </AnimatePresence>\n\n          {rules.length > 0 ? (\n            <ul className=\"grid gap-1.5 sm:grid-cols-2\" aria-label=\"Password requirements\">\n              {rules.map((rule) => {\n                const met = rule.test(value);\n                return (\n                  <li\n                    key={rule.id}\n                    className={cn(\n                      \"flex items-center gap-2 text-xs transition-colors duration-200\",\n                      met ? \"text-foreground\" : \"text-muted-foreground\",\n                    )}\n                  >\n                    <RequirementMark met={met} reduce={Boolean(reduce)} />\n                    <span>{rule.label}</span>\n                    <span className=\"sr-only\">{met ? \"(met)\" : \"(not met)\"}</span>\n                  </li>\n                );\n              })}\n            </ul>\n          ) : null}\n        </div>\n      </div>\n    );\n  },\n);\n\n/** Eye that morphs to eye-off: a slash draws across and the pupil dims. */\nfunction EyeMorph({ closed, reduce }: { closed: boolean; reduce: boolean }) {\n  const transition = reduce ? { duration: 0 } : { duration: 0.22, ease: EASE_OUT };\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={2}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      aria-hidden=\"true\"\n    >\n      <path d=\"M2.06 12.35a1 1 0 0 1 0-.7 10.75 10.75 0 0 1 19.88 0 1 1 0 0 1 0 .7 10.75 10.75 0 0 1-19.88 0\" />\n      <motion.circle\n        cx={12}\n        cy={12}\n        r={3}\n        initial={false}\n        animate={{ opacity: closed ? 0.35 : 1 }}\n        transition={transition}\n      />\n      <motion.path\n        d=\"M3 3l18 18\"\n        initial={false}\n        animate={{ pathLength: closed ? 1 : 0, opacity: closed ? 1 : 0 }}\n        transition={transition}\n      />\n    </svg>\n  );\n}\n\nfunction RequirementMark({ met, reduce }: { met: boolean; reduce: boolean }) {\n  return (\n    <span\n      aria-hidden=\"true\"\n      className={cn(\n        \"grid size-4 shrink-0 place-items-center rounded-full border transition-colors duration-200\",\n        met\n          ? \"border-(--color-success) bg-(--color-success) text-background\"\n          : \"border-muted-foreground/40 bg-transparent text-transparent\",\n      )}\n    >\n      <svg viewBox=\"0 0 24 24\" fill=\"none\" className=\"size-3\" aria-hidden=\"true\">\n        <motion.path\n          d=\"M5 12.5l4.5 4.5L19 7.5\"\n          stroke=\"currentColor\"\n          strokeWidth={3}\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          initial={false}\n          animate={{ pathLength: met ? 1 : 0 }}\n          transition={reduce ? { duration: 0 } : { duration: 0.25, ease: EASE_OUT }}\n        />\n      </svg>\n    </span>\n  );\n}\n\nfunction CapsIcon() {\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={2}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      className=\"size-3.5\"\n      aria-hidden=\"true\"\n    >\n      <path d=\"M12 4 4 12h4v4h8v-4h4z\" />\n      <path d=\"M8 20h8\" />\n    </svg>\n  );\n}\n"},{"path":"components/motion/input.tsx","type":"registry:component","target":"@components/motion/input.tsx","content":"\"use client\";\n\nimport {\n  AnimatePresence,\n  animate,\n  motion,\n  useReducedMotion,\n} from \"motion/react\";\nimport {\n  forwardRef,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  type InputHTMLAttributes,\n  type ReactNode,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type InputClassNames = {\n  root?: string;\n  label?: string;\n  field?: string;\n  input?: string;\n  leftIcon?: string;\n  rightIcon?: string;\n  successIcon?: string;\n  errorMessage?: string;\n};\n\nexport interface InputProps extends Omit<\n  InputHTMLAttributes<HTMLInputElement>,\n  \"value\" | \"defaultValue\" | \"onChange\"\n> {\n  label?: string;\n  value?: string;\n  defaultValue?: string;\n  onChange?: (value: string) => void;\n  /** Truthy error triggers a shake, red border and (if a string) a message. */\n  error?: string | boolean;\n  success?: boolean;\n  leftIcon?: ReactNode;\n  rightIcon?: ReactNode;\n  className?: string;\n  classNames?: InputClassNames;\n}\n\nexport const Input = forwardRef<HTMLInputElement, InputProps>(function Input(\n  {\n    label,\n    value: valueProp,\n    defaultValue,\n    onChange,\n    onFocus,\n    onBlur,\n    error,\n    success,\n    leftIcon,\n    rightIcon,\n    className,\n    classNames,\n    disabled,\n    id: idProp,\n    type,\n    ...rest\n  },\n  ref,\n) {\n  const reactId = useId();\n  const id = idProp ?? reactId;\n  const reduce = useReducedMotion();\n\n  const controlled = valueProp !== undefined;\n  const [internal, setInternal] = useState(defaultValue ?? \"\");\n  const value = controlled ? (valueProp ?? \"\") : internal;\n\n  const [focused, setFocused] = useState(false);\n\n  const fieldRef = useRef<HTMLDivElement>(null);\n\n  const hasError = Boolean(error);\n  const errorMessage = typeof error === \"string\" ? error : null;\n\n  // Right edge shows the success check, otherwise the caller's right icon.\n  const rightSlot = success ? null : rightIcon;\n\n  // Shake the field when an error appears.\n  useEffect(() => {\n    if (!fieldRef.current || reduce || !hasError) return;\n    animate(\n      fieldRef.current,\n      { x: [0, -6, 6, -4, 4, -2, 0] },\n      { duration: 0.45 },\n    );\n  }, [hasError, reduce]);\n\n  const handleChange = (next: string) => {\n    if (!controlled) setInternal(next);\n    onChange?.(next);\n  };\n\n  return (\n    <div\n      className={cn(\"flex flex-col gap-1.5\", className, classNames?.root)}\n    >\n      {label ? (\n        <label\n          htmlFor={id}\n          className={cn(\n            \"px-1 text-sm font-medium text-foreground\",\n            classNames?.label,\n          )}\n        >\n          {label}\n        </label>\n      ) : null}\n\n      <div\n        ref={fieldRef}\n        data-state={\n          hasError\n            ? \"error\"\n            : success\n              ? \"success\"\n              : focused\n                ? \"focused\"\n                : \"idle\"\n        }\n        className={cn(\n          \"relative h-11 overflow-hidden rounded-full border transition-colors duration-200\",\n          \"border-border\",\n          focused && !hasError && \"border-foreground/40 ring-2 ring-ring/40\",\n          hasError && \"border-destructive ring-2 ring-destructive/25\",\n          disabled && \"opacity-60\",\n          classNames?.field,\n        )}\n      >\n        {leftIcon ? (\n          <span\n            className={cn(\n              \"pointer-events-none absolute left-3 top-1/2 flex -translate-y-1/2 items-center text-muted-foreground [&_svg]:h-4 [&_svg]:w-4\",\n              classNames?.leftIcon,\n            )}\n          >\n            {leftIcon}\n          </span>\n        ) : null}\n\n        <input\n          ref={ref}\n          id={id}\n          type={type}\n          value={value}\n          disabled={disabled}\n          aria-invalid={hasError || undefined}\n          aria-describedby={errorMessage ? `${id}-error` : undefined}\n          {...rest}\n          onChange={(e) => handleChange(e.target.value)}\n          onFocus={(event) => {\n            setFocused(true);\n            onFocus?.(event);\n          }}\n          onBlur={(event) => {\n            setFocused(false);\n            onBlur?.(event);\n          }}\n          className={cn(\n            \"peer h-full w-full bg-transparent text-base leading-6 text-foreground caret-foreground outline-none\",\n            \"placeholder:text-muted-foreground/60\",\n            leftIcon ? \"pl-10\" : \"pl-3.5\",\n            rightSlot || success ? \"pr-10\" : \"pr-3.5\",\n            disabled && \"cursor-not-allowed\",\n            classNames?.input,\n          )}\n        />\n\n        {success ? (\n          <motion.svg\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            className={cn(\n              \"absolute right-3.5 top-1/2 h-5 w-5 -translate-y-1/2 text-(--color-success)\",\n              classNames?.successIcon,\n            )}\n          >\n            <motion.path\n              d=\"M5 12.5l4.5 4.5L19 7.5\"\n              stroke=\"currentColor\"\n              strokeWidth={2.5}\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              initial={reduce ? { pathLength: 1 } : { pathLength: 0 }}\n              animate={{ pathLength: 1 }}\n              transition={{ duration: 0.35, ease: \"easeOut\" }}\n            />\n          </motion.svg>\n        ) : rightSlot ? (\n          <span\n            className={cn(\n              \"absolute right-0 top-0 flex h-full items-center text-muted-foreground [&_button]:grid [&_button]:size-11 [&_button]:place-items-center [&_svg]:h-4 [&_svg]:w-4\",\n              classNames?.rightIcon,\n            )}\n          >\n            {rightSlot}\n          </span>\n        ) : null}\n      </div>\n\n      <AnimatePresence initial={false}>\n        {errorMessage ? (\n          <motion.p\n            id={`${id}-error`}\n            role=\"alert\"\n            initial={\n              reduce\n                ? { opacity: 0 }\n                : { opacity: 0, y: -4, filter: \"blur(4px)\" }\n            }\n            animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n            exit={\n              reduce\n                ? { opacity: 0 }\n                : { opacity: 0, y: -4, filter: \"blur(4px)\" }\n            }\n            transition={{ duration: 0.2 }}\n            className={cn(\n              \"px-1 text-xs text-destructive\",\n              classNames?.errorMessage,\n            )}\n          >\n            {errorMessage}\n          </motion.p>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  );\n});\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n\n/** Dragged handles and fills (sliders) — critically damped `useSpring` config,\n * so the value follows the pointer butterily and never rebounds off an end. */\nexport const SPRING_GLIDE = {\n  stiffness: 700,\n  damping: 50,\n  mass: 0.5,\n} as const;\n"},{"path":"lib/password-strength.ts","type":"registry:lib","target":"@lib/password-strength.ts","content":"// Shared password strength heuristic, used by SignUpForm and PasswordInput.\n\nexport const MIN_PASSWORD_LENGTH = 8;\n\n/** Label per score, index 0-4. */\nexport const STRENGTH_LABELS = [\n  \"Too short\",\n  \"Weak\",\n  \"Fair\",\n  \"Good\",\n  \"Strong\",\n] as const;\n\n/** Fill color per score, index 0-4 (Tailwind background classes). */\nexport const STRENGTH_COLORS = [\n  \"bg-destructive\",\n  \"bg-destructive\",\n  \"bg-amber-500\",\n  \"bg-amber-400\",\n  \"bg-(--color-success)\",\n] as const;\n\nexport type PasswordStrength = 0 | 1 | 2 | 3 | 4;\n\n/**\n * Length-weighted strength score, 0-4. NIST SP 800-63B advises against\n * composition requirements and treats length as the dominant factor, so extra\n * character classes only nudge the score — they can't rescue a short password.\n * This is a heuristic for feedback, not entropy estimation; pair it with a\n * breach-list check server-side for anything real.\n */\nexport function passwordStrength(\n  password: string,\n  minLength: number = MIN_PASSWORD_LENGTH,\n): PasswordStrength {\n  if (password.length < minLength) return 0;\n\n  let score = 1;\n  if (password.length >= 12) score += 1;\n  if (password.length >= 16) score += 1;\n\n  const classes = [/[a-z]/, /[A-Z]/, /\\d/, /[^A-Za-z0-9]/].filter((pattern) =>\n    pattern.test(password),\n  ).length;\n  if (classes >= 3) score += 1;\n\n  return Math.min(score, 4) as PasswordStrength;\n}\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}