{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "code-entry",
  "type": "registry:component",
  "title": "Code Entry",
  "description": "A six-digit code field with a ring that glides from slot to slot, room to paste the whole code, a small shake when it's wrong and slots that flow together into a “Verified” pill when it's right.",
  "categories": [
    "component",
    "Forms"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://coss.com/ui/r/spinner.json"
  ],
  "files": [
    {
      "path": "registry/devl/code-entry.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { CheckIcon } from \"lucide-react\";\nimport { AnimatePresence, motion, useAnimationControls, useReducedMotion } from \"motion/react\";\nimport { Fragment, useEffect, useId, useLayoutEffect, useRef, useState } from \"react\";\nimport { Spinner } from \"@/components/ui/spinner\";\nimport { cn } from \"@/lib/utils\";\n\ntype Status = \"idle\" | \"checking\" | \"error\" | \"success\";\n\nexport interface CodeEntryProps {\n  length?: number;\n  groupSize?: number;\n  onVerify?: (code: string) => boolean | Promise<boolean>;\n  onVerified?: (code: string) => void;\n  label?: string;\n  hint?: string;\n  errorMessage?: string;\n  successLabel?: string;\n  autoFocus?: boolean;\n  className?: string;\n}\n\nconst SLOT_W = 44;\nconst SLOT_H = 52;\nconst PILL_W = 148;\nconst PILL_H = 44;\n\nexport function CodeEntry({\n  length = 6,\n  groupSize = 3,\n  onVerify,\n  onVerified,\n  label = \"Verification code\",\n  hint = \"Enter the 6-digit code we sent you.\",\n  errorMessage = \"That code didn’t match. Try again.\",\n  successLabel = \"Verified\",\n  autoFocus = false,\n  className,\n}: CodeEntryProps) {\n  const [code, setCode] = useState(\"\");\n  const [status, setStatus] = useState<Status>(\"idle\");\n  const [focused, setFocused] = useState(false);\n  const [slots, setSlots] = useState<number[]>([]);\n  const [width, setWidth] = useState(0);\n  const inputRef = useRef<HTMLInputElement>(null);\n  const rowRef = useRef<HTMLDivElement>(null);\n  const slotRefs = useRef<(HTMLDivElement | null)[]>([]);\n  const timers = useRef<number[]>([]);\n  const shake = useAnimationControls();\n  const reduce = useReducedMotion();\n  const id = useId();\n  const filterId = `code-entry-goo-${id.replace(/:/g, \"\")}`;\n\n  useLayoutEffect(() => {\n    const measure = () => {\n      setSlots(slotRefs.current.map((el) => el?.offsetLeft ?? 0));\n      setWidth(rowRef.current?.offsetWidth ?? 0);\n    };\n    measure();\n    const observer = new ResizeObserver(measure);\n    if (rowRef.current) observer.observe(rowRef.current);\n    return () => observer.disconnect();\n  }, [length, groupSize]);\n\n  useEffect(() => () => timers.current.forEach(clearTimeout), []);\n\n  const later = (fn: () => void, ms: number) => {\n    timers.current.push(window.setTimeout(fn, ms));\n  };\n\n  const verify = async (value: string) => {\n    setStatus(\"checking\");\n    const ok = onVerify ? await onVerify(value) : true;\n    if (ok) {\n      setStatus(\"success\");\n      inputRef.current?.blur();\n      later(() => onVerified?.(value), 900);\n      return;\n    }\n    setStatus(\"error\");\n    if (!reduce) void shake.start({ x: [0, -7, 6, -4, 2, 0], transition: { duration: 0.5, ease: \"easeInOut\" } });\n    later(() => {\n      setCode(\"\");\n      inputRef.current?.focus();\n    }, 900);\n  };\n\n  const onChange = (raw: string) => {\n    if (status === \"checking\" || status === \"success\") return;\n    const next = raw.replace(/\\D/g, \"\").slice(0, length);\n    setCode(next);\n    if (status === \"error\") setStatus(\"idle\");\n    if (next.length === length) void verify(next);\n  };\n\n  const active = Math.min(code.length, length - 1);\n  const ringX = slots[active] ?? 0;\n  const spring = reduce ? { duration: 0 } : { type: \"spring\" as const, stiffness: 520, damping: 36 };\n  const success = status === \"success\";\n  const pillX = width / 2 - PILL_W / 2;\n\n  return (\n    <div className={cn(\"grid justify-items-center gap-3\", className)}>\n      <div className=\"grid justify-items-center gap-1 text-center\">\n        <label className=\"font-medium text-sm\" htmlFor={id}>\n          {label}\n        </label>\n        <p className=\"text-muted-foreground text-xs\" id={`${id}-hint`}>\n          {hint}\n        </p>\n      </div>\n\n      <motion.div animate={shake} className=\"relative\" ref={rowRef} style={{ height: SLOT_H }}>\n        <motion.div\n          animate={{ opacity: success ? 0 : 1 }}\n          className=\"flex items-center gap-2\"\n          transition={{ duration: success ? 0.15 : 0.3 }}\n        >\n          {Array.from({ length }, (_, i) => {\n            const char = code[i];\n            const isActive = focused && i === active && status !== \"checking\";\n            return (\n              <Fragment key={i}>\n                {groupSize > 0 && i > 0 && i % groupSize === 0 && (\n                  <span aria-hidden className=\"h-0.5 w-2.5 rounded-full bg-border\" />\n                )}\n                <div\n                  className={cn(\n                    \"relative flex items-center justify-center rounded-xl border bg-background font-medium font-mono text-xl tabular-nums shadow-xs/5 transition-colors duration-300 dark:bg-input/32\",\n                    status === \"error\" && \"border-destructive/60 bg-destructive/4 text-destructive-foreground\",\n                    status === \"checking\" && \"text-muted-foreground\",\n                  )}\n                  ref={(el) => {\n                    slotRefs.current[i] = el;\n                  }}\n                  style={{ width: SLOT_W, height: SLOT_H }}\n                >\n                  <AnimatePresence initial={false}>\n                    {char && (\n                      <motion.span\n                        animate={{ scale: 1, opacity: 1, y: 0 }}\n                        exit={{ scale: 0.9, opacity: 0, transition: { duration: 0.1 } }}\n                        initial={reduce ? { opacity: 0 } : { scale: 0.9, opacity: 0, y: 4 }}\n                        key={`${i}-${char}`}\n                        transition={{ duration: 0.16, ease: [0.23, 1, 0.32, 1] }}\n                        className=\"absolute\"\n                      >\n                        {char}\n                      </motion.span>\n                    )}\n                  </AnimatePresence>\n                  {isActive && !char && (\n                    <span aria-hidden className=\"h-6 w-px animate-caret-blink bg-foreground\" />\n                  )}\n                </div>\n              </Fragment>\n            );\n          })}\n        </motion.div>\n\n        <motion.div\n          animate={{ x: ringX, opacity: focused && !success && status !== \"checking\" ? 1 : 0 }}\n          aria-hidden\n          className={cn(\n            \"pointer-events-none absolute top-0 left-0 rounded-xl border-2 border-ring ring-4 ring-ring/16\",\n            status === \"error\" && \"border-destructive/70 ring-destructive/16\",\n          )}\n          initial={false}\n          style={{ width: SLOT_W, height: SLOT_H }}\n          transition={{ x: spring, opacity: { duration: 0.15 } }}\n        />\n\n        <AnimatePresence>\n          {status === \"checking\" && (\n            <motion.div\n              animate={{ opacity: 1 }}\n              className=\"pointer-events-none absolute inset-0 flex items-center justify-center\"\n              exit={{ opacity: 0 }}\n              initial={{ opacity: 0 }}\n            >\n              <span className=\"flex size-8 items-center justify-center rounded-full bg-popover shadow-xs/5 ring-1 ring-border\">\n                <Spinner className=\"size-4\" />\n              </span>\n            </motion.div>\n          )}\n        </AnimatePresence>\n\n        {success && (\n          <div aria-hidden className=\"pointer-events-none absolute inset-0\">\n            <svg className=\"absolute size-0\">\n              <filter colorInterpolationFilters=\"sRGB\" height=\"200%\" id={filterId} width=\"140%\" x=\"-20%\" y=\"-50%\">\n                <feGaussianBlur in=\"SourceGraphic\" result=\"blur\" stdDeviation=\"6\" />\n                <feColorMatrix in=\"blur\" result=\"goo\" values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 22 -9\" />\n                <feComposite in=\"SourceGraphic\" in2=\"goo\" operator=\"atop\" />\n              </filter>\n            </svg>\n            <div className=\"absolute inset-0\" style={{ filter: `url(#${filterId})` }}>\n              {slots.map((x, i) => (\n                <motion.span\n                  animate={{ x: pillX, y: (SLOT_H - PILL_H) / 2, width: PILL_W, height: PILL_H, borderRadius: PILL_H / 2, opacity: 1 }}\n                  className=\"absolute top-0 left-0 bg-success\"\n                  initial={reduce ? false : { x, y: 0, width: SLOT_W, height: SLOT_H, borderRadius: 12, opacity: 0 }}\n                  key={i}\n                  transition={\n                    reduce\n                      ? { duration: 0 }\n                      : {\n                          type: \"spring\",\n                          stiffness: 260,\n                          damping: 28,\n                          delay: Math.abs(i - (length - 1) / 2) * 0.04,\n                          opacity: { duration: 0.12 },\n                        }\n                  }\n                />\n              ))}\n            </div>\n            <motion.div\n              animate={{ opacity: 1, scale: 1 }}\n              className=\"absolute inset-0 flex items-center justify-center gap-1.5 font-medium text-sm text-white\"\n              initial={reduce ? false : { opacity: 0, scale: 0.85 }}\n              transition={{ delay: reduce ? 0 : 0.3, type: \"spring\", stiffness: 400, damping: 24 }}\n            >\n              <motion.span\n                animate={{ rotate: 0, scale: 1, opacity: 1 }}\n                className=\"flex size-5 items-center justify-center rounded-full bg-white/20\"\n                initial={reduce ? false : { rotate: -45, scale: 0.6, opacity: 0 }}\n                transition={{ delay: reduce ? 0 : 0.36, type: \"spring\", stiffness: 500, damping: 26 }}\n              >\n                <CheckIcon className=\"size-3.5\" strokeWidth={3} />\n              </motion.span>\n              {successLabel}\n            </motion.div>\n          </div>\n        )}\n\n        <input\n          aria-describedby={`${id}-hint ${id}-status`}\n          aria-invalid={status === \"error\" || undefined}\n          autoComplete=\"one-time-code\"\n          autoFocus={autoFocus}\n          className=\"absolute inset-0 z-10 w-full cursor-text bg-transparent text-transparent caret-transparent outline-none selection:bg-transparent disabled:cursor-default\"\n          disabled={success}\n          id={id}\n          inputMode=\"numeric\"\n          maxLength={length}\n          onBlur={() => setFocused(false)}\n          onChange={(e) => onChange(e.target.value)}\n          onFocus={(e) => {\n            setFocused(true);\n            const end = e.target.value.length;\n            e.target.setSelectionRange(end, end);\n          }}\n          onSelect={(e) => {\n            const el = e.currentTarget;\n            const end = el.value.length;\n            if (el.selectionStart !== end) el.setSelectionRange(end, end);\n          }}\n          pattern=\"\\d*\"\n          ref={inputRef}\n          spellCheck={false}\n          value={code}\n        />\n      </motion.div>\n\n      <p\n        aria-live=\"polite\"\n        className={cn(\n          \"h-4 text-xs transition-colors\",\n          status === \"error\" ? \"text-destructive-foreground\" : success ? \"text-success-foreground\" : \"text-muted-foreground\",\n        )}\n        id={`${id}-status`}\n      >\n        {status === \"error\" ? errorMessage : status === \"checking\" ? \"Checking…\" : success ? \"You’re all set.\" : \"\"}\n      </p>\n    </div>\n  );\n}\n"
    }
  ]
}