{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "input-62",
  "title": "Confirm password with match state",
  "description": "The confirmation stays quiet while it follows the password and shows a cross or a check once it differs or matches.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/field.json",
    "https://ui.ndekocode.dev/r/icons.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-62.tsx",
      "content": "'use client';\n\nimport {\n  AnimatePresence,\n  motion,\n  type Transition,\n  type Variants,\n} from 'motion/react';\nimport { useState, type ChangeEvent } from 'react';\n\nimport { usePrefersReducedMotion } from '@/registry/ndeko/hooks/use-prefers-reduced-motion';\nimport { CheckIcon, XIcon } from '@/registry/ndeko/lib/icons';\nimport { cn } from '@/registry/ndeko/lib/utils';\nimport { Field, FieldDescription, FieldLabel } from '@/registry/ndeko/ui/field';\nimport { Input } from '@/registry/ndeko/ui/input';\n\ntype MatchState = 'empty' | 'on-track' | 'mismatch' | 'match';\n\nconst SWAP_TRANSITION: Transition = { duration: 0.15, ease: [0.2, 0, 0, 1] };\n\nconst SWAP_VARIANTS: Variants = {\n  enter: { opacity: 0, scale: 0.6, filter: 'blur(2px)' },\n  visible: { opacity: 1, scale: 1, filter: 'blur(0px)' },\n  exit: { opacity: 0, scale: 0.6, filter: 'blur(2px)' },\n};\n\nconst FADE_VARIANTS: Variants = {\n  enter: { opacity: 0 },\n  visible: { opacity: 1 },\n  exit: { opacity: 0 },\n};\n\nconst MESSAGES: Record<MatchState, { text: string; tone: string }> = {\n  empty: { text: 'Type the same password again', tone: '' },\n  'on-track': { text: 'Type the same password again', tone: '' },\n  mismatch: {\n    text: 'Passwords do not match',\n    tone: 'text-destructive-ink',\n  },\n  match: { text: 'Passwords match', tone: 'text-success-foreground' },\n};\n\nfunction swapVariantsOf(shouldReduceMotion: boolean | null) {\n  if (shouldReduceMotion) {\n    return FADE_VARIANTS;\n  }\n  return SWAP_VARIANTS;\n}\n\nfunction matchStateOf(password: string, confirmation: string): MatchState {\n  if (confirmation.length === 0) {\n    return 'empty';\n  }\n  if (confirmation === password) {\n    return 'match';\n  }\n  const isStillTyping = password.startsWith(confirmation);\n  if (isStillTyping) {\n    return 'on-track';\n  }\n  return 'mismatch';\n}\n\nfunction MatchGlyph({ state }: { state: MatchState }) {\n  if (state === 'match') {\n    return <CheckIcon className='text-success-foreground size-4' />;\n  }\n  return <XIcon className='text-destructive-ink size-4' />;\n}\n\nfunction announcementOf(state: MatchState) {\n  const isSettled = state === 'match';\n  const isWrong = state === 'mismatch';\n  const isWorthAnnouncing = isSettled || isWrong;\n  if (!isWorthAnnouncing) {\n    return '';\n  }\n  return MESSAGES[state].text;\n}\n\nexport default function Input62() {\n  const [password, setPassword] = useState('');\n  const [confirmation, setConfirmation] = useState('');\n  const shouldReduceMotion = usePrefersReducedMotion();\n  const swapVariants = swapVariantsOf(shouldReduceMotion);\n  const state = matchStateOf(password, confirmation);\n  const message = MESSAGES[state];\n  const isMismatch = state === 'mismatch';\n  const isMatch = state === 'match';\n  const showsGlyph = isMismatch || isMatch;\n\n  function changePassword(event: ChangeEvent<HTMLInputElement>) {\n    setPassword(event.currentTarget.value);\n  }\n\n  function changeConfirmation(event: ChangeEvent<HTMLInputElement>) {\n    setConfirmation(event.currentTarget.value);\n  }\n\n  return (\n    <div className='flex w-full max-w-xs flex-col gap-4'>\n      <Field>\n        <FieldLabel>New password</FieldLabel>\n        <Input\n          type='password'\n          autoComplete='new-password'\n          placeholder='At least 12 characters'\n          value={password}\n          onChange={changePassword}\n        />\n      </Field>\n      <Field invalid={isMismatch}>\n        <FieldLabel>Confirm password</FieldLabel>\n        <div className='relative'>\n          <Input\n            className='pe-8'\n            type='password'\n            autoComplete='new-password'\n            placeholder='Repeat the password'\n            value={confirmation}\n            onChange={changeConfirmation}\n          />\n          <span\n            aria-hidden='true'\n            className='pointer-events-none absolute inset-y-0 end-2 flex items-center'\n          >\n            <AnimatePresence mode='popLayout' initial={false}>\n              {showsGlyph && (\n                <motion.span\n                  key={state}\n                  className='flex'\n                  variants={swapVariants}\n                  initial='enter'\n                  animate='visible'\n                  exit='exit'\n                  transition={SWAP_TRANSITION}\n                >\n                  <MatchGlyph state={state} />\n                </motion.span>\n              )}\n            </AnimatePresence>\n          </span>\n        </div>\n        <FieldDescription className={cn('transition-colors', message.tone)}>\n          {message.text}\n        </FieldDescription>\n      </Field>\n      <span className='sr-only' role='status'>\n        {announcementOf(state)}\n      </span>\n    </div>\n  );\n}\n",
      "type": "registry:component"
    }
  ],
  "categories": [
    "components",
    "input"
  ],
  "type": "registry:component"
}