{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "p-otp-field-6",
  "description": "OTP field with custom sanitization",
  "registryDependencies": [
    "@coss/otp-field",
    "@coss/field"
  ],
  "files": [
    {
      "path": "registry/default/particles/p-otp-field-6.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport {\n  Field,\n  FieldDescription,\n  FieldLabel,\n} from \"@/registry/default/ui/field\";\nimport { OTPField, OTPFieldInput } from \"@/registry/default/ui/otp-field\";\n\nconst OTP_LENGTH = 6;\n\nconst OTP_SLOT_KEYS = Array.from(\n  { length: OTP_LENGTH },\n  (_, i) => `otp-slot-${i}`,\n);\n\nfunction sanitizeTierCode(value: string) {\n  return value.replace(/[^0-3]/g, \"\");\n}\n\nexport default function Particle() {\n  const [focusedIndex, setFocusedIndex] = useState(0);\n  const [invalidPulse, setInvalidPulse] = useState(0);\n  const [statusMessage, setStatusMessage] = useState(\"\");\n  const invalidTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n  const skipClearOnNextValueChangeRef = useRef(false);\n\n  useEffect(() => {\n    return () => {\n      if (invalidTimeoutRef.current != null) {\n        clearTimeout(invalidTimeoutRef.current);\n      }\n    };\n  }, []);\n\n  function clearInvalidFeedback() {\n    if (invalidTimeoutRef.current != null) {\n      clearTimeout(invalidTimeoutRef.current);\n      invalidTimeoutRef.current = null;\n    }\n    setInvalidPulse(0);\n    setStatusMessage(\"\");\n  }\n\n  function handleValueChange() {\n    if (skipClearOnNextValueChangeRef.current) {\n      skipClearOnNextValueChangeRef.current = false;\n      return;\n    }\n    clearInvalidFeedback();\n  }\n\n  function handleValueInvalid(value: string) {\n    skipClearOnNextValueChangeRef.current = true;\n    setInvalidPulse((current) => current + 1);\n    setStatusMessage(`Unsupported characters were ignored from ${value}.`);\n\n    if (invalidTimeoutRef.current != null) {\n      clearTimeout(invalidTimeoutRef.current);\n    }\n    invalidTimeoutRef.current = setTimeout(() => {\n      invalidTimeoutRef.current = null;\n      setInvalidPulse(0);\n    }, 400);\n  }\n\n  const activeInvalidIndex = invalidPulse > 0 ? focusedIndex : -1;\n\n  return (\n    <Field className=\"items-center\">\n      <FieldLabel>Tier code</FieldLabel>\n      <OTPField\n        inputMode=\"numeric\"\n        length={OTP_LENGTH}\n        sanitizeValue={sanitizeTierCode}\n        validationType=\"none\"\n        onValueChange={handleValueChange}\n        onValueInvalid={handleValueInvalid}\n      >\n        {OTP_SLOT_KEYS.map((slotKey, index) => {\n          const showInvalid = activeInvalidIndex === index && invalidPulse > 0;\n\n          return (\n            <OTPFieldInput\n              key={slotKey}\n              aria-invalid={showInvalid || undefined}\n              aria-label={`Character ${index + 1} of ${OTP_LENGTH}`}\n              onFocus={() => {\n                setFocusedIndex(index);\n              }}\n            />\n          );\n        })}\n      </OTPField>\n      <FieldDescription>Digits 0-3 only.</FieldDescription>\n      <span aria-live=\"polite\" className=\"sr-only\">\n        {statusMessage}\n      </span>\n    </Field>\n  );\n}\n",
      "type": "registry:block"
    }
  ],
  "categories": [
    "otp field",
    "input",
    "field",
    "validation"
  ],
  "type": "registry:block"
}