{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "otp-field",
  "title": "Otp Field",
  "description": "A otp-field component.",
  "files": [
    {
      "path": "registry/default/otp-field/otp-field.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { OTPField as BaseOTPField } from \"@base-ui/react/otp-field\";\n\nimport { cn } from \"@/lib/utils\";\n\nimport \"./otp-field.css\";\n\nfunction OTPField({\n  className,\n  ...props\n}: React.ComponentProps<typeof BaseOTPField.Root>) {\n  return (\n    <BaseOTPField.Root\n      data-slot=\"otp-field\"\n      className={cn(\"flex items-center gap-2\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction OTPFieldInput({\n  className,\n  variant = \"default\",\n  ...props\n}: React.ComponentProps<typeof BaseOTPField.Input> & {\n  variant?: \"default\" | \"elevated\";\n}) {\n  return (\n    <BaseOTPField.Input\n      data-slot=\"otp-field-input\"\n      className={cn(\n        // Base styling\n        \"flex h-10 w-10 items-center justify-center rounded-lg border bg-clip-padding text-center text-base font-medium sm:h-9 sm:w-9 md:text-sm\",\n        variant === \"default\" ? \"bg-input\" : \"bg-input-elevated\",\n        // Selection\n        \"selection:bg-primary selection:text-primary-foreground\",\n        // Placeholder\n        \"placeholder:text-muted-foreground focus:placeholder:text-transparent\",\n        // :focus not data-[focused] — data-[focused] propagates to all inputs\n        // in the group, not just the active one.\n        \"outline-0 outline-offset-0 outline-transparent transition-[outline-width,outline-offset,outline-color] duration-100 ease-out outline-solid\",\n        \"focus:outline-ring/50 focus:outline-2 focus:outline-offset-2\",\n        // Invalid state\n        \"aria-invalid:outline-destructive/50 aria-invalid:outline-2 aria-invalid:outline-offset-2 aria-invalid:outline-solid\",\n        // Disabled state\n        \"data-[disabled]:pointer-events-none data-[disabled]:cursor-not-allowed data-[disabled]:opacity-60\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction OTPFieldSeparator({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof BaseOTPField.Separator>) {\n  return (\n    <BaseOTPField.Separator\n      data-slot=\"otp-field-separator\"\n      className={cn(\"text-muted-foreground\", className)}\n      {...props}\n    >\n      {children ?? <SeparatorDash />}\n    </BaseOTPField.Separator>\n  );\n}\n\nfunction OTPFieldGroup({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"otp-field-group\"\n      className={cn(\"flex items-center gap-2\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SeparatorDash(props: React.ComponentProps<\"svg\">) {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      viewBox=\"0 0 8 2\"\n      width=\"8\"\n      height=\"2\"\n      fill=\"none\"\n      {...props}\n    >\n      <path\n        d=\"M1 1H7\"\n        stroke=\"currentColor\"\n        strokeWidth=\"1.5\"\n        strokeLinecap=\"round\"\n      />\n    </svg>\n  );\n}\n\nexport { OTPField, OTPFieldInput, OTPFieldSeparator, OTPFieldGroup };\n",
      "type": "registry:ui",
      "target": "components/ui/cubby-ui/otp-field/otp-field.tsx"
    },
    {
      "path": "registry/default/otp-field/hooks/use-invalid-feedback.ts",
      "content": "\"use client\";\r\n\r\nimport * as React from \"react\";\r\n\r\nexport interface UseInvalidFeedbackOptions {\r\n  /**\r\n   * Duration in ms before the shake animation clears.\r\n   * @default 400\r\n   */\r\n  duration?: number;\r\n}\r\n\r\nexport interface UseInvalidFeedbackReturn {\r\n  /**\r\n   * Returns the invalid animation className for a given input index,\r\n   * or `undefined` when no feedback is active. Merge with `cn()`.\r\n   *\r\n   * @example\r\n   * ```tsx\r\n   * <OTPFieldInput className={invalidFeedback.getInvalidClassName(index)} />\r\n   * ```\r\n   */\r\n  getInvalidClassName: (index: number) => string | undefined;\r\n\r\n  /**\r\n   * Call in each input's `onFocus` to track which slot is active.\r\n   *\r\n   * @example\r\n   * ```tsx\r\n   * <OTPFieldInput onFocus={() => invalidFeedback.setFocusedIndex(index)} />\r\n   * ```\r\n   */\r\n  setFocusedIndex: (index: number) => void;\r\n\r\n  /**\r\n   * Clears invalid feedback when the value changes.\r\n   * Wire to OTPField's `onValueChange`.\r\n   *\r\n   * @example\r\n   * ```tsx\r\n   * <OTPField onValueChange={invalidFeedback.handleValueChange} />\r\n   * ```\r\n   */\r\n  handleValueChange: () => void;\r\n\r\n  /**\r\n   * Triggers invalid feedback animation and status message.\r\n   * Wire to OTPField's `onValueInvalid`.\r\n   *\r\n   * @example\r\n   * ```tsx\r\n   * <OTPField onValueInvalid={invalidFeedback.handleValueInvalid} />\r\n   * ```\r\n   */\r\n  handleValueInvalid: (value: string) => void;\r\n\r\n  /**\r\n   * Screen reader status message. Render inside an `aria-live` region.\r\n   *\r\n   * @example\r\n   * ```tsx\r\n   * <span aria-live=\"polite\" className=\"sr-only\">\r\n   *   {invalidFeedback.statusMessage}\r\n   * </span>\r\n   * ```\r\n   */\r\n  statusMessage: string;\r\n}\r\n\r\nconst SHAKE_CLASS_A = \"otp-field-shake-a focus:outline-destructive/80\";\r\nconst SHAKE_CLASS_B = \"otp-field-shake-b focus:outline-destructive/80\";\r\n\r\n/**\r\n * Hook that manages shake animation and screen reader feedback\r\n * for invalid OTP input.\r\n *\r\n * @example\r\n * ```tsx\r\n * const invalidFeedback = useInvalidFeedback();\r\n *\r\n * <OTPField\r\n *   length={6}\r\n *   validationType=\"none\"\r\n *   normalizeValue={normalizeTierCode}\r\n *   onValueChange={invalidFeedback.handleValueChange}\r\n *   onValueInvalid={invalidFeedback.handleValueInvalid}\r\n * >\r\n *   {Array.from({ length: 6 }, (_, index) => (\r\n *     <OTPFieldInput\r\n *       key={index}\r\n *       className={invalidFeedback.getInvalidClassName(index)}\r\n *       onFocus={() => invalidFeedback.setFocusedIndex(index)}\r\n *     />\r\n *   ))}\r\n * </OTPField>\r\n * <span aria-live=\"polite\" className=\"sr-only\">\r\n *   {invalidFeedback.statusMessage}\r\n * </span>\r\n * ```\r\n */\r\nexport function useInvalidFeedback(\r\n  options: UseInvalidFeedbackOptions = {},\r\n): UseInvalidFeedbackReturn {\r\n  const { duration = 400 } = options;\r\n\r\n  const [focusedIndex, setFocusedIndex] = React.useState(0);\r\n  const [invalidPulse, setInvalidPulse] = React.useState(0);\r\n  const [statusMessage, setStatusMessage] = React.useState(\"\");\r\n  const invalidTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(\r\n    null,\r\n  );\r\n  const skipClearOnNextValueChangeRef = React.useRef(false);\r\n\r\n  React.useEffect(() => {\r\n    return () => {\r\n      if (invalidTimeoutRef.current != null) {\r\n        clearTimeout(invalidTimeoutRef.current);\r\n      }\r\n    };\r\n  }, []);\r\n\r\n  function handleValueChange() {\r\n    if (skipClearOnNextValueChangeRef.current) {\r\n      skipClearOnNextValueChangeRef.current = false;\r\n      return;\r\n    }\r\n\r\n    if (invalidTimeoutRef.current != null) {\r\n      clearTimeout(invalidTimeoutRef.current);\r\n      invalidTimeoutRef.current = null;\r\n    }\r\n    setInvalidPulse(0);\r\n    setStatusMessage(\"\");\r\n  }\r\n\r\n  function handleValueInvalid(value: string) {\r\n    skipClearOnNextValueChangeRef.current = true;\r\n    setInvalidPulse((current) => current + 1);\r\n    setStatusMessage(`Unsupported characters were ignored from ${value}.`);\r\n\r\n    if (invalidTimeoutRef.current != null) {\r\n      clearTimeout(invalidTimeoutRef.current);\r\n    }\r\n\r\n    invalidTimeoutRef.current = setTimeout(() => {\r\n      invalidTimeoutRef.current = null;\r\n      setInvalidPulse(0);\r\n    }, duration);\r\n  }\r\n\r\n  const activeInvalidIndex = invalidPulse > 0 ? focusedIndex : -1;\r\n  const invalidClassName =\r\n    invalidPulse === 0\r\n      ? undefined\r\n      : invalidPulse % 2 === 0\r\n        ? SHAKE_CLASS_B\r\n        : SHAKE_CLASS_A;\r\n\r\n  const getInvalidClassName = React.useCallback(\r\n    (index: number) =>\r\n      activeInvalidIndex === index ? invalidClassName : undefined,\r\n    [activeInvalidIndex, invalidClassName],\r\n  );\r\n\r\n  return {\r\n    getInvalidClassName,\r\n    setFocusedIndex,\r\n    handleValueChange,\r\n    handleValueInvalid,\r\n    statusMessage,\r\n  };\r\n}\r\n",
      "type": "registry:hook",
      "target": "components/ui/cubby-ui/otp-field/hooks/use-invalid-feedback.ts"
    },
    {
      "path": "registry/default/otp-field/otp-field.css",
      "content": "/* -------------------------------------------------------------------------------------------------\n * OTP field invalid-input shake animation.\n *\n * Paired identical keyframes. CSS animations don't restart when re-applied to\n * the same element with the same animation-name, so on each invalid OTP entry\n * we alternate between the two classes to force the browser to treat it as a\n * new animation and replay the shake. Keep both bodies in sync.\n * -------------------------------------------------------------------------------------------------*/\n\n.otp-field-shake-a {\n  animation: otp-field-shake-a 180ms ease-in-out;\n}\n\n.otp-field-shake-b {\n  animation: otp-field-shake-b 180ms ease-in-out;\n}\n\n@keyframes otp-field-shake-a {\n  0%,\n  100% {\n    transform: translateX(0);\n  }\n  25% {\n    transform: translateX(-4px);\n  }\n  75% {\n    transform: translateX(4px);\n  }\n}\n\n@keyframes otp-field-shake-b {\n  0%,\n  100% {\n    transform: translateX(0);\n  }\n  25% {\n    transform: translateX(-4px);\n  }\n  75% {\n    transform: translateX(4px);\n  }\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .otp-field-shake-a,\n  .otp-field-shake-b {\n    animation: none;\n  }\n}\n",
      "type": "registry:file",
      "target": "components/ui/cubby-ui/otp-field/otp-field.css"
    }
  ],
  "type": "registry:ui"
}