{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "input-63",
  "title": "Segmented strength meter",
  "description": "Four segments fill in turn as the password gets stronger and the strength label slides in the same direction.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/field.json",
    "https://ui.ndekocode.dev/r/input.json",
    "https://ui.ndekocode.dev/r/theme.json",
    "https://ui.ndekocode.dev/r/use-prefers-reduced-motion.json",
    "https://ui.ndekocode.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "registry/ndeko/components/input/input-63.tsx",
      "content": "'use client';\n\nimport {\n  AnimatePresence,\n  motion,\n  type Transition,\n  type Variants,\n} from 'motion/react';\nimport { useId, useState, type ChangeEvent } from 'react';\n\nimport { usePrefersReducedMotion } from '@/registry/ndeko/hooks/use-prefers-reduced-motion';\nimport { cn } from '@/registry/ndeko/lib/utils';\nimport { Field, FieldLabel } from '@/registry/ndeko/ui/field';\nimport { Input } from '@/registry/ndeko/ui/input';\n\nconst SEGMENTS = [0, 1, 2, 3];\nconst MAX_LEVEL = SEGMENTS.length;\nconst MIN_LENGTH = 12;\nconst SEGMENT_STAGGER_MS = 40;\nconst LABEL_SHIFT_PX = 8;\n\nconst LEVELS = [\n  { label: 'Not set', tone: 'bg-transparent', ink: 'text-muted-foreground' },\n  { label: 'Weak', tone: 'bg-destructive', ink: 'text-destructive-ink' },\n  { label: 'Fair', tone: 'bg-warning', ink: 'text-warning-foreground' },\n  { label: 'Good', tone: 'bg-success', ink: 'text-success-foreground' },\n  { label: 'Strong', tone: 'bg-success', ink: 'text-success-foreground' },\n];\n\nconst SEGMENT_FILL =\n  'h-full w-full origin-left rounded-full transition-[scale,background-color] duration-200 ease-(--ease-out-strong) rtl:origin-right';\n\nconst LABEL_TRANSITION: Transition = { duration: 0.18, ease: [0.2, 0, 0, 1] };\n\nconst LABEL_VARIANTS: Variants = {\n  enter: (direction: number) => {\n    const offset = direction * LABEL_SHIFT_PX;\n    return { opacity: 0, y: offset };\n  },\n  visible: { opacity: 1, y: 0 },\n  exit: (direction: number) => {\n    const offset = direction * LABEL_SHIFT_PX;\n    return { opacity: 0, y: -offset };\n  },\n};\n\nconst FADE_VARIANTS: Variants = {\n  enter: { opacity: 0 },\n  visible: { opacity: 1 },\n  exit: { opacity: 0 },\n};\n\nfunction labelVariantsOf(shouldReduceMotion: boolean | null) {\n  if (shouldReduceMotion) {\n    return FADE_VARIANTS;\n  }\n  return LABEL_VARIANTS;\n}\n\nfunction hasMixedCase(password: string) {\n  const hasLower = /[a-z]/.test(password);\n  const hasUpper = /[A-Z]/.test(password);\n  return hasLower && hasUpper;\n}\n\nfunction levelOf(password: string) {\n  if (password.length === 0) {\n    return 0;\n  }\n  const checks = [\n    password.length >= MIN_LENGTH,\n    hasMixedCase(password),\n    /\\d/.test(password),\n    /[^A-Za-z0-9]/.test(password),\n  ];\n  const passed = checks.filter(Boolean).length;\n  return Math.max(1, passed);\n}\n\nfunction directionBetween(previousLevel: number, nextLevel: number) {\n  if (nextLevel < previousLevel) {\n    return -1;\n  }\n  return 1;\n}\n\nfunction segmentDelayOf(index: number, direction: number) {\n  if (direction > 0) {\n    const delay = index * SEGMENT_STAGGER_MS;\n    return { transitionDelay: `${delay}ms` };\n  }\n  const reversedIndex = MAX_LEVEL - 1 - index;\n  const delay = reversedIndex * SEGMENT_STAGGER_MS;\n  return { transitionDelay: `${delay}ms` };\n}\n\nfunction segmentFillOf(index: number, level: number) {\n  const isFilled = index < level;\n  if (isFilled) {\n    return 'scale-x-100';\n  }\n  return 'scale-x-0';\n}\n\nexport default function Input63() {\n  const labelId = useId();\n  const [password, setPassword] = useState('');\n  const [direction, setDirection] = useState(1);\n  const shouldReduceMotion = usePrefersReducedMotion();\n  const labelVariants = labelVariantsOf(shouldReduceMotion);\n  const level = levelOf(password);\n  const current = LEVELS[level];\n\n  function changePassword(event: ChangeEvent<HTMLInputElement>) {\n    const nextPassword = event.currentTarget.value;\n    const nextLevel = levelOf(nextPassword);\n    setDirection(directionBetween(level, nextLevel));\n    setPassword(nextPassword);\n  }\n\n  return (\n    <div className='flex w-full max-w-xs flex-col gap-3'>\n      <Field>\n        <FieldLabel>Password</FieldLabel>\n        <Input\n          type='password'\n          autoComplete='new-password'\n          placeholder='Choose a password'\n          value={password}\n          onChange={changePassword}\n        />\n      </Field>\n      <div className='flex flex-col gap-2'>\n        <div\n          className='flex gap-1.5'\n          role='meter'\n          aria-labelledby={labelId}\n          aria-valuemin={0}\n          aria-valuemax={MAX_LEVEL}\n          aria-valuenow={level}\n          aria-valuetext={current.label}\n        >\n          {SEGMENTS.map((index) => (\n            <span\n              key={index}\n              className='bg-foreground/[0.08] h-1 flex-1 overflow-hidden rounded-full'\n            >\n              <span\n                className={cn(\n                  SEGMENT_FILL,\n                  segmentFillOf(index, level),\n                  current.tone\n                )}\n                style={segmentDelayOf(index, direction)}\n              />\n            </span>\n          ))}\n        </div>\n        <div className='flex items-center justify-between text-xs'>\n          <span id={labelId} className='text-muted-foreground'>\n            Password strength\n          </span>\n          <span className='relative flex font-medium' aria-hidden='true'>\n            <AnimatePresence\n              mode='popLayout'\n              initial={false}\n              custom={direction}\n            >\n              <motion.span\n                key={current.label}\n                className={cn('flex', current.ink)}\n                custom={direction}\n                variants={labelVariants}\n                initial='enter'\n                animate='visible'\n                exit='exit'\n                transition={LABEL_TRANSITION}\n              >\n                {current.label}\n              </motion.span>\n            </AnimatePresence>\n          </span>\n        </div>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component"
    }
  ],
  "categories": [
    "components",
    "input"
  ],
  "type": "registry:component"
}