{
  "$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\";\n\nimport * as React from \"react\";\n\nexport interface UseInvalidFeedbackOptions {\n  /**\n   * Duration in ms before the shake animation clears.\n   * @default 400\n   */\n  duration?: number;\n}\n\nexport interface UseInvalidFeedbackReturn {\n  /**\n   * Returns the invalid animation className for a given input index,\n   * or `undefined` when no feedback is active. Merge with `cn()`.\n   *\n   * @example\n   * ```tsx\n   * <OTPFieldInput className={invalidFeedback.getInvalidClassName(index)} />\n   * ```\n   */\n  getInvalidClassName: (index: number) => string | undefined;\n\n  /**\n   * Call in each input's `onFocus` to track which slot is active.\n   *\n   * @example\n   * ```tsx\n   * <OTPFieldInput onFocus={() => invalidFeedback.setFocusedIndex(index)} />\n   * ```\n   */\n  setFocusedIndex: (index: number) => void;\n\n  /**\n   * Clears invalid feedback when the value changes.\n   * Wire to OTPField's `onValueChange`.\n   *\n   * @example\n   * ```tsx\n   * <OTPField onValueChange={invalidFeedback.handleValueChange} />\n   * ```\n   */\n  handleValueChange: () => void;\n\n  /**\n   * Triggers invalid feedback animation and status message.\n   * Wire to OTPField's `onValueInvalid`.\n   *\n   * @example\n   * ```tsx\n   * <OTPField onValueInvalid={invalidFeedback.handleValueInvalid} />\n   * ```\n   */\n  handleValueInvalid: (value: string) => void;\n\n  /**\n   * Screen reader status message. Render inside an `aria-live` region.\n   *\n   * @example\n   * ```tsx\n   * <span aria-live=\"polite\" className=\"sr-only\">\n   *   {invalidFeedback.statusMessage}\n   * </span>\n   * ```\n   */\n  statusMessage: string;\n}\n\nconst SHAKE_CLASS_A = \"otp-field-shake-a focus:outline-destructive/80\";\nconst SHAKE_CLASS_B = \"otp-field-shake-b focus:outline-destructive/80\";\n\n/**\n * Hook that manages shake animation and screen reader feedback\n * for invalid OTP input.\n *\n * @example\n * ```tsx\n * const invalidFeedback = useInvalidFeedback();\n *\n * <OTPField\n *   length={6}\n *   validationType=\"none\"\n *   normalizeValue={normalizeTierCode}\n *   onValueChange={invalidFeedback.handleValueChange}\n *   onValueInvalid={invalidFeedback.handleValueInvalid}\n * >\n *   {Array.from({ length: 6 }, (_, index) => (\n *     <OTPFieldInput\n *       key={index}\n *       className={invalidFeedback.getInvalidClassName(index)}\n *       onFocus={() => invalidFeedback.setFocusedIndex(index)}\n *     />\n *   ))}\n * </OTPField>\n * <span aria-live=\"polite\" className=\"sr-only\">\n *   {invalidFeedback.statusMessage}\n * </span>\n * ```\n */\nexport function useInvalidFeedback(\n  options: UseInvalidFeedbackOptions = {},\n): UseInvalidFeedbackReturn {\n  const { duration = 400 } = options;\n\n  const [focusedIndex, setFocusedIndex] = React.useState(0);\n  const [invalidPulse, setInvalidPulse] = React.useState(0);\n  const [statusMessage, setStatusMessage] = React.useState(\"\");\n  const invalidTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(\n    null,\n  );\n  const skipClearOnNextValueChangeRef = React.useRef(false);\n\n  React.useEffect(() => {\n    return () => {\n      if (invalidTimeoutRef.current != null) {\n        clearTimeout(invalidTimeoutRef.current);\n      }\n    };\n  }, []);\n\n  function handleValueChange() {\n    if (skipClearOnNextValueChangeRef.current) {\n      skipClearOnNextValueChangeRef.current = false;\n      return;\n    }\n\n    if (invalidTimeoutRef.current != null) {\n      clearTimeout(invalidTimeoutRef.current);\n      invalidTimeoutRef.current = null;\n    }\n    setInvalidPulse(0);\n    setStatusMessage(\"\");\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\n    invalidTimeoutRef.current = setTimeout(() => {\n      invalidTimeoutRef.current = null;\n      setInvalidPulse(0);\n    }, duration);\n  }\n\n  const activeInvalidIndex = invalidPulse > 0 ? focusedIndex : -1;\n  const invalidClassName =\n    invalidPulse === 0\n      ? undefined\n      : invalidPulse % 2 === 0\n        ? SHAKE_CLASS_B\n        : SHAKE_CLASS_A;\n\n  const getInvalidClassName = React.useCallback(\n    (index: number) =>\n      activeInvalidIndex === index ? invalidClassName : undefined,\n    [activeInvalidIndex, invalidClassName],\n  );\n\n  return {\n    getInvalidClassName,\n    setFocusedIndex,\n    handleValueChange,\n    handleValueInvalid,\n    statusMessage,\n  };\n}\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"
}