{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "input-65",
  "title": "Password with a generator",
  "description": "A password field with a segmented strength meter and a live rules list, a Generate button that scrambles in a strong password, a Copy button, and a spoken strength.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/button.json",
    "https://ui.ndekocode.dev/r/chat-motion.json",
    "https://ui.ndekocode.dev/r/demonstration.json",
    "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/live-region.json",
    "https://ui.ndekocode.dev/r/theme.json",
    "https://ui.ndekocode.dev/r/use-autoplay.json",
    "https://ui.ndekocode.dev/r/use-kept-height.json",
    "https://ui.ndekocode.dev/r/use-prefers-reduced-motion.json",
    "https://ui.ndekocode.dev/r/use-timeouts.json",
    "https://ui.ndekocode.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "registry/ndeko/components/input/input-65.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 {\n  typingSteps,\n  useAutoplay,\n  type AutoplayStep,\n} from '@/registry/ndeko/hooks/use-autoplay';\nimport { useKeptHeight } from '@/registry/ndeko/hooks/use-kept-height';\nimport { usePrefersReducedMotion } from '@/registry/ndeko/hooks/use-prefers-reduced-motion';\nimport { useTimeouts } from '@/registry/ndeko/hooks/use-timeouts';\nimport {\n  PRESS_LEAD_MS,\n  pressedQuietOf,\n  spokenUnless,\n  usePress,\n} from '@/registry/ndeko/lib/demonstration';\nimport {\n  CheckIcon,\n  CircleIcon,\n  CopyIcon,\n  EyeIcon,\n  EyeOffIcon,\n  KeyIcon,\n} from '@/registry/ndeko/lib/icons';\nimport { cn } from '@/registry/ndeko/lib/utils';\nimport { Button } from '@/registry/ndeko/ui/button';\nimport { Swap } from '@/registry/ndeko/ui/chat-motion';\nimport { Field, FieldLabel } from '@/registry/ndeko/ui/field';\nimport { Input } from '@/registry/ndeko/ui/input';\nimport { LiveRegion } from '@/registry/ndeko/ui/live-region';\n\nconst SEGMENTS = [0, 1, 2, 3];\nconst MAX_LEVEL = SEGMENTS.length;\nconst MIN_LENGTH = 12;\nconst GENERATED_LENGTH = 16;\nconst SCRAMBLE_FRAME_MS = 45;\nconst SCRAMBLE_MS = (GENERATED_LENGTH + 1) * SCRAMBLE_FRAME_MS;\nconst SEGMENT_STAGGER_MS = 40;\nconst LABEL_SHIFT_PX = 8;\nconst COPIED_FOR_MS = 1600;\nconst SAMPLE_PASSWORD = 'T7#mQ2!xLw9@Rk4v';\n\nconst LOWER = 'abcdefghijkmnopqrstuvwxyz';\nconst UPPER = 'ABCDEFGHJKLMNPQRSTUVWXYZ';\nconst DIGITS = '23456789';\nconst SYMBOLS = '!@#$%&*?';\nconst POOL = `${LOWER}${UPPER}${DIGITS}${SYMBOLS}`;\n\ntype Rule = { label: string; isMetBy: (password: string) => boolean };\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\nconst RULES: Rule[] = [\n  {\n    label: `${MIN_LENGTH}+ characters`,\n    isMetBy: (password) => password.length >= MIN_LENGTH,\n  },\n  { label: 'Upper and lower case', isMetBy: hasMixedCase },\n  { label: 'A number', isMetBy: (password) => /\\d/.test(password) },\n  { label: 'A symbol', isMetBy: (password) => /[^A-Za-z0-9]/.test(password) },\n];\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 levelOf(password: string) {\n  if (password.length === 0) {\n    return 0;\n  }\n  const passed = RULES.filter((rule) => rule.isMetBy(password)).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\nfunction randomIndexOf(size: number) {\n  const values = new Uint32Array(1);\n  crypto.getRandomValues(values);\n  return values[0] % size;\n}\n\nfunction randomCharOf(source: string) {\n  return source[randomIndexOf(source.length)];\n}\n\nfunction shuffled(characters: string[]) {\n  const result = [...characters];\n  for (let index = result.length - 1; index > 0; index -= 1) {\n    const swapIndex = randomIndexOf(index + 1);\n    const held = result[index];\n    result[index] = result[swapIndex];\n    result[swapIndex] = held;\n  }\n  return result;\n}\n\n/** One character of each kind first, so the result always meets every rule. */\nfunction strongPasswordOf(length: number) {\n  const required = [\n    randomCharOf(LOWER),\n    randomCharOf(UPPER),\n    randomCharOf(DIGITS),\n    randomCharOf(SYMBOLS),\n  ];\n  const characters = [...required];\n  while (characters.length < length) {\n    characters.push(randomCharOf(POOL));\n  }\n  return shuffled(characters).join('');\n}\n\nfunction scrambleFrameOf(target: string, settledCount: number) {\n  const settled = target.slice(0, settledCount);\n  const restCount = target.length - settledCount;\n  let rest = '';\n  for (let index = 0; index < restCount; index += 1) {\n    rest += randomCharOf(POOL);\n  }\n  return `${settled}${rest}`;\n}\n\nfunction strengthMessageOf(level: number) {\n  return `Password strength: ${LEVELS[level].label}`;\n}\n\nconst GENERATED_MESSAGE = `Strong password generated. ${strengthMessageOf(MAX_LEVEL)}`;\n\nfunction ruleStatusOf(isMet: boolean) {\n  if (isMet) {\n    return 'met';\n  }\n  return 'not met yet';\n}\n\nfunction inputTypeOf(isShown: boolean) {\n  if (isShown) {\n    return 'text';\n  }\n  return 'password';\n}\n\nfunction revealLabelOf(isRevealed: boolean) {\n  if (isRevealed) {\n    return 'Hide password';\n  }\n  return 'Show password';\n}\n\nfunction RuleRow({ label, isMet }: { label: string; isMet: boolean }) {\n  const glyphKey = String(isMet);\n\n  return (\n    <li\n      className={cn(\n        'flex items-center gap-1.5 text-xs transition-colors duration-150',\n        isMet && 'text-foreground',\n        !isMet && 'text-muted-foreground'\n      )}\n    >\n      <Swap swapKey={glyphKey}>\n        {isMet && <CheckIcon className='text-success-foreground size-3.5' />}\n        {!isMet && <CircleIcon className='size-3.5 opacity-60' />}\n      </Swap>\n      {label}\n      <span className='sr-only'>, {ruleStatusOf(isMet)}</span>\n    </li>\n  );\n}\n\nexport default function Input65() {\n  const [password, setPassword] = useState(SAMPLE_PASSWORD);\n  const [isRevealed, setIsRevealed] = useState(false);\n  const [isScrambling, setIsScrambling] = useState(false);\n  const [isCopied, setIsCopied] = useState(false);\n  const [direction, setDirection] = useState(1);\n  const [announcement, setAnnouncement] = useState('');\n  const { pressed, press } = usePress<'generate' | 'copy'>();\n  const { schedule, clearAll } = useTimeouts();\n  const shouldReduceMotion = usePrefersReducedMotion();\n  const labelVariants = labelVariantsOf(shouldReduceMotion);\n  const level = levelOf(password);\n  const current = LEVELS[level];\n  const showsText = isRevealed || isScrambling;\n\n  function writePassword(next: string) {\n    const nextLevel = levelOf(next);\n    setDirection(directionBetween(level, nextLevel));\n    setPassword(next);\n  }\n\n  function changePassword(event: ChangeEvent<HTMLInputElement>) {\n    const next = event.currentTarget.value;\n    const nextLevel = levelOf(next);\n    clearAll();\n    setIsScrambling(false);\n    writePassword(next);\n    if (nextLevel !== level) {\n      setAnnouncement(strengthMessageOf(nextLevel));\n    }\n  }\n\n  function generate() {\n    const target = strongPasswordOf(GENERATED_LENGTH);\n    clearAll();\n    setIsCopied(false);\n    if (shouldReduceMotion) {\n      writePassword(target);\n      return 0;\n    }\n    setIsScrambling(true);\n    for (let settled = 0; settled <= GENERATED_LENGTH; settled += 1) {\n      const frame = scrambleFrameOf(target, settled);\n      schedule(() => writePassword(frame), settled * SCRAMBLE_FRAME_MS);\n    }\n    schedule(() => setIsScrambling(false), SCRAMBLE_MS);\n    return SCRAMBLE_MS;\n  }\n\n  function generateFromPerson() {\n    const durationMs = generate();\n    schedule(() => setAnnouncement(GENERATED_MESSAGE), durationMs);\n  }\n\n  function showCopied() {\n    setIsCopied(true);\n    schedule(() => setIsCopied(false), COPIED_FOR_MS);\n  }\n\n  async function copy() {\n    try {\n      await navigator.clipboard.writeText(password);\n    } catch {\n      setAnnouncement('The password could not be copied');\n      return;\n    }\n    showCopied();\n    setAnnouncement('Password copied');\n  }\n\n  function beginTyping() {\n    clearAll();\n    setIsScrambling(false);\n    setIsCopied(false);\n    setIsRevealed(true);\n    writePassword('');\n  }\n\n  function startOver() {\n    clearAll();\n    setIsScrambling(false);\n    setIsCopied(false);\n    setIsRevealed(false);\n    setDirection(1);\n    setPassword(SAMPLE_PASSWORD);\n  }\n\n  const steps: AutoplayStep[] = [\n    { after: 2600, run: beginTyping },\n    ...typingSteps('Sunrise2026', writePassword, {\n      startAfter: 600,\n      characterMs: 85,\n    }),\n    { after: 1500, run: () => press('generate') },\n    { after: PRESS_LEAD_MS, run: () => generate() },\n    { after: 2400, run: () => press('copy') },\n    { after: PRESS_LEAD_MS, run: showCopied },\n  ];\n  const { ref, isAutoplaying } = useAutoplay(steps, {\n    loopAfter: 2600,\n    onReset: startOver,\n  });\n\n  useKeptHeight(ref);\n\n  const spokenMessage = spokenUnless(isAutoplaying, announcement);\n  const copyLabel = 'Copy the password';\n\n  return (\n    <div\n      ref={ref}\n      className='@container flex min-h-[17rem] w-full max-w-xs flex-col gap-3'\n    >\n      <Field>\n        <FieldLabel>Password</FieldLabel>\n        <div className='relative'>\n          <Input\n            type={inputTypeOf(showsText)}\n            autoComplete='new-password'\n            placeholder='Choose a password'\n            value={password}\n            onChange={changePassword}\n            className='pe-9'\n          />\n          <Button\n            variant='ghost'\n            size='icon-sm'\n            aria-label={revealLabelOf(isRevealed)}\n            aria-pressed={isRevealed}\n            onClick={() => setIsRevealed(!isRevealed)}\n            className='text-muted-foreground hover:text-foreground absolute end-0.5 top-0.5 size-7'\n          >\n            <Swap swapKey={String(isRevealed)}>\n              {isRevealed && <EyeOffIcon className='size-4' />}\n              {!isRevealed && <EyeIcon className='size-4' />}\n            </Swap>\n          </Button>\n        </div>\n      </Field>\n      <div className='flex gap-2'>\n        <Button\n          variant='outline'\n          size='sm'\n          onClick={generateFromPerson}\n          className={pressedQuietOf(pressed === 'generate')}\n        >\n          <KeyIcon className='size-3.5 opacity-60' />\n          Generate\n        </Button>\n        <Button\n          variant='outline'\n          size='sm'\n          disabled={password.length === 0}\n          onClick={copy}\n          aria-label={copyLabel}\n          className={pressedQuietOf(pressed === 'copy')}\n        >\n          <Swap swapKey={String(isCopied)}>\n            {isCopied && (\n              <CheckIcon className='text-success-foreground size-3.5' />\n            )}\n            {!isCopied && <CopyIcon className='size-3.5 opacity-60' />}\n          </Swap>\n          Copy\n        </Button>\n      </div>\n      <div className='flex flex-col gap-2'>\n        <div\n          className='flex gap-1.5'\n          role='meter'\n          aria-label='Password strength'\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 className='text-muted-foreground'>Password strength</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      <ul\n        aria-label='Password rules'\n        className='grid gap-1.5 @[17rem]:grid-cols-2'\n      >\n        {RULES.map((rule) => (\n          <RuleRow\n            key={rule.label}\n            label={rule.label}\n            isMet={rule.isMetBy(password)}\n          />\n        ))}\n      </ul>\n      <LiveRegion message={spokenMessage} />\n    </div>\n  );\n}\n",
      "type": "registry:component"
    }
  ],
  "categories": [
    "components",
    "input"
  ],
  "type": "registry:component"
}