{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "otp-field",
  "type": "registry:ui",
  "title": "OTP Field",
  "description": "One-time-code input built on Base UI.",
  "dependencies": [
    "@base-ui/react",
    "motion"
  ],
  "registryDependencies": [
    "https://seamui.dev/r/utils.json",
    "https://seamui.dev/r/motion.json",
    "https://seamui.dev/r/haptics.json"
  ],
  "files": [
    {
      "path": "registry/seam/ui/otp-field.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { OTPField as BaseOTPField } from \"@base-ui/react/otp-field\"\nimport { motion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { springs, fades, shake, reduced, useReducedMotion } from \"@/lib/motion\"\nimport { useHaptics } from \"@/lib/haptics\"\n\nconst MotionInput = motion.create(BaseOTPField.Input)\n\n// Entry wells don't press (§1 — slots, not keys), but they do react: each\n// slot pops as its digit lands (opacity-only under reduced motion), and the\n// whole group shakes when `invalid` flips on (reduced: an opacity flash —\n// the destructive border below carries the state either way). The caret is\n// the browser's own — every slot is a real <input>, so nothing fake to blink.\nfunction OTPField({\n  className,\n  length = 6,\n  invalid,\n  value,\n  defaultValue,\n  onValueChange,\n  ...props\n}: React.ComponentProps<typeof BaseOTPField.Root> & { invalid?: boolean }) {\n  const reduceMotion = useReducedMotion() ?? false\n  const { trigger } = useHaptics()\n  const [internalLen, setInternalLen] = React.useState(() =>\n    typeof defaultValue === \"string\" ? defaultValue.length : 0\n  )\n  const filled = typeof value === \"string\" ? value.length : internalLen\n  const filledRef = React.useRef(filled)\n  filledRef.current = filled\n\n  // tactile feedback: the error pattern when a code is rejected.\n  React.useEffect(() => {\n    if (invalid) trigger(\"error\")\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [invalid])\n\n  return (\n    <BaseOTPField.Root\n      data-slot=\"otp-field\"\n      data-invalid={invalid || undefined}\n      length={length}\n      value={value}\n      defaultValue={defaultValue}\n      onValueChange={(\n        ...args: Parameters<NonNullable<typeof onValueChange>>\n      ) => {\n        // tactile feedback: a tick per digit landed (not on delete).\n        if (args[0].length > filledRef.current) trigger(\"tick\")\n        setInternalLen(args[0].length)\n        onValueChange?.(...args)\n      }}\n      render={\n        <motion.div\n          animate={\n            invalid\n              ? reduceMotion\n                ? reduced.flash.animate\n                : shake.animate\n              : undefined\n          }\n          transition={\n            reduceMotion ? reduced.flash.transition : shake.transition\n          }\n        />\n      }\n      className={cn(\"flex items-center gap-2\", className)}\n      {...props}\n    >\n      {/* Render one Input per slot; each derives its index from DOM order. */}\n      {Array.from({ length }).map((_, i) => (\n        <MotionInput\n          key={i}\n          data-slot=\"otp-field-input\"\n          aria-invalid={invalid || undefined}\n          initial={false}\n          animate={i < filled ? \"filled\" : \"empty\"}\n          variants={\n            reduceMotion\n              ? { filled: { opacity: [0.5, 1] }, empty: { opacity: 1 } }\n              : { filled: { scale: [1.12, 1] }, empty: { scale: 1 } }\n          }\n          transition={reduceMotion ? fades.fast : springs.snappy}\n          className={cn(\n            \"size-10 rounded-md squircle border border-border/60 bg-muted text-center text-sm shadow-well outline-none\",\n            \"focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/50\",\n            \"data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n            invalid &&\n              \"border-destructive focus-visible:border-destructive focus-visible:ring-destructive/30\"\n          )}\n        />\n      ))}\n    </BaseOTPField.Root>\n  )\n}\n\nexport { OTPField }\n",
      "type": "registry:ui",
      "target": "components/ui/otp-field.tsx"
    }
  ]
}