{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "auth-otp-verify",
  "title": "Auth OTP Verify",
  "description": "Verify OTP code sent via email, SMS, or WhatsApp.",
  "registryDependencies": [
    "button",
    "card",
    "field",
    "input-otp",
    "select"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/auth/auth-otp-verify.tsx",
      "content": "\"use client\";\n\nimport { Loader2, Mail, MessageSquare, Phone, RefreshCw } from \"lucide-react\";\nimport { useCallback, useEffect, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/registry/new-york/ui/button\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/registry/new-york/ui/card\";\nimport {\n  Field,\n  FieldContent,\n  FieldDescription,\n  FieldError,\n  FieldLabel,\n} from \"@/registry/new-york/ui/field\";\nimport {\n  InputOTP,\n  InputOTPGroup,\n  InputOTPSlot,\n} from \"@/registry/new-york/ui/input-otp\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/registry/new-york/ui/select\";\n\nexport type OTPDeliveryMethod = \"email\" | \"sms\" | \"whatsapp\";\n\nexport interface AuthOTPVerifyProps {\n  deliveryMethod?: OTPDeliveryMethod;\n  deliveryAddress?: string;\n  onDeliveryMethodChange?: (method: OTPDeliveryMethod) => void;\n  onSubmit?: (code: string) => void;\n  onResend?: (method: OTPDeliveryMethod) => void;\n  className?: string;\n  isLoading?: boolean;\n  resendCooldown?: number;\n  errors?: {\n    code?: string;\n    general?: string;\n  };\n  autoSubmit?: boolean;\n  codeLength?: number;\n  availableMethods?: OTPDeliveryMethod[];\n}\n\nconst DELIVERY_METHOD_CONFIG: Record<\n  OTPDeliveryMethod,\n  { label: string; icon: React.ComponentType<{ className?: string }> }\n> = {\n  email: {\n    label: \"Email\",\n    icon: Mail,\n  },\n  sms: {\n    label: \"SMS\",\n    icon: MessageSquare,\n  },\n  whatsapp: {\n    label: \"WhatsApp\",\n    icon: Phone,\n  },\n};\n\nfunction formatDeliveryAddress(\n  address: string | undefined,\n  method: OTPDeliveryMethod\n): string {\n  if (!address) return \"\";\n  if (method === \"email\") return address;\n  if (address.length > 4) {\n    const visible = address.slice(-4);\n    const masked = \"*\".repeat(address.length - 4);\n    return `${masked}${visible}`;\n  }\n  return address;\n}\n\ninterface ErrorAlertProps {\n  message: string;\n}\n\nfunction ErrorAlert({ message }: ErrorAlertProps) {\n  return (\n    <div\n      aria-live=\"polite\"\n      className=\"rounded-lg border border-destructive/50 bg-destructive/10 p-3 text-destructive text-sm\"\n      role=\"alert\"\n    >\n      {message}\n    </div>\n  );\n}\n\ninterface DeliveryMethodSelectProps {\n  availableMethods: OTPDeliveryMethod[];\n  deliveryMethod: OTPDeliveryMethod;\n  onDeliveryMethodChange: (method: OTPDeliveryMethod) => void;\n}\n\nfunction DeliveryMethodSelect({\n  availableMethods,\n  deliveryMethod,\n  onDeliveryMethodChange,\n}: DeliveryMethodSelectProps) {\n  return (\n    <Field>\n      <FieldLabel>Delivery method</FieldLabel>\n      <FieldContent>\n        <Select\n          onValueChange={(value) =>\n            onDeliveryMethodChange(value as OTPDeliveryMethod)\n          }\n          value={deliveryMethod}\n        >\n          <SelectTrigger className=\"w-full\">\n            <SelectValue />\n          </SelectTrigger>\n          <SelectContent>\n            {availableMethods.map((method) => {\n              const config = DELIVERY_METHOD_CONFIG[method];\n              const Icon = config.icon;\n              return (\n                <SelectItem key={method} value={method}>\n                  <div className=\"flex items-center gap-2\">\n                    <Icon aria-hidden=\"true\" className=\"size-4\" />\n                    {config.label}\n                  </div>\n                </SelectItem>\n              );\n            })}\n          </SelectContent>\n        </Select>\n        <FieldDescription>\n          Choose how you want to receive the verification code\n        </FieldDescription>\n      </FieldContent>\n    </Field>\n  );\n}\n\ninterface ResendButtonProps {\n  cooldown: number;\n  isLoading: boolean;\n  onClick: () => void;\n}\n\nfunction ResendButton({ cooldown, isLoading, onClick }: ResendButtonProps) {\n  return (\n    <button\n      className=\"min-h-[32px] touch-manipulation self-start rounded-sm hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 sm:self-auto\"\n      disabled={cooldown > 0 || isLoading}\n      onClick={onClick}\n      type=\"button\"\n    >\n      {cooldown > 0 ? (\n        `Resend in ${cooldown}s`\n      ) : (\n        <span className=\"flex items-center gap-1\">\n          <RefreshCw aria-hidden=\"true\" className=\"size-3\" />\n          Resend code\n        </span>\n      )}\n    </button>\n  );\n}\n\ninterface OTPFieldProps {\n  code: string;\n  codeError?: string;\n  codeLength: number;\n  isLoading: boolean;\n  onCodeChange: (code: string) => void;\n  onResend?: () => void;\n  resendCooldown: number;\n}\n\nfunction OTPField({\n  code,\n  codeError,\n  codeLength,\n  isLoading,\n  onCodeChange,\n  onResend,\n  resendCooldown,\n}: OTPFieldProps) {\n  return (\n    <Field data-invalid={!!codeError}>\n      <FieldLabel htmlFor=\"otp-code\">\n        Verification code\n        <span aria-label=\"required\" className=\"text-destructive\">\n          *\n        </span>\n      </FieldLabel>\n      <FieldContent>\n        <InputOTP\n          aria-describedby={codeError ? \"otp-code-error\" : undefined}\n          aria-invalid={!!codeError}\n          disabled={isLoading}\n          id=\"otp-code\"\n          maxLength={codeLength}\n          onChange={onCodeChange}\n          value={code}\n        >\n          <InputOTPGroup>\n            {Array.from({ length: codeLength }).map((_, index) => (\n              <InputOTPSlot index={index} key={index} />\n            ))}\n          </InputOTPGroup>\n        </InputOTP>\n        {codeError && <FieldError id=\"otp-code-error\">{codeError}</FieldError>}\n        <div className=\"flex flex-col gap-2 text-muted-foreground text-xs sm:flex-row sm:items-center sm:justify-between\">\n          <span>Enter the {codeLength}-digit code</span>\n          {onResend && (\n            <ResendButton\n              cooldown={resendCooldown}\n              isLoading={isLoading}\n              onClick={onResend}\n            />\n          )}\n        </div>\n      </FieldContent>\n    </Field>\n  );\n}\n\ninterface VerifyButtonProps {\n  code: string;\n  codeLength: number;\n  isLoading: boolean;\n  onSubmit: (code: string) => void;\n}\n\nfunction VerifyButton({\n  code,\n  codeLength,\n  isLoading,\n  onSubmit,\n}: VerifyButtonProps) {\n  return (\n    <Button\n      aria-busy={isLoading}\n      className=\"min-h-[44px] w-full touch-manipulation\"\n      data-loading={isLoading}\n      disabled={isLoading || code.length !== codeLength}\n      onClick={() => onSubmit(code)}\n      type=\"button\"\n    >\n      {isLoading ? (\n        <>\n          <Loader2 aria-hidden=\"true\" className=\"size-4 animate-spin\" />\n          Verifying…\n        </>\n      ) : (\n        \"Verify code\"\n      )}\n    </Button>\n  );\n}\n\nexport default function AuthOTPVerify({\n  deliveryMethod = \"email\",\n  deliveryAddress,\n  onDeliveryMethodChange,\n  onSubmit,\n  onResend,\n  className,\n  isLoading = false,\n  resendCooldown = 60,\n  errors,\n  autoSubmit = true,\n  codeLength = 6,\n  availableMethods = [\"email\", \"sms\"],\n}: AuthOTPVerifyProps) {\n  const [code, setCode] = useState(\"\");\n  const [cooldown, setCooldown] = useState(0);\n\n  useEffect(() => {\n    if (cooldown > 0) {\n      const timer = setTimeout(() => {\n        setCooldown((prev) => prev - 1);\n      }, 1000);\n      return () => clearTimeout(timer);\n    }\n  }, [cooldown]);\n\n  useEffect(() => {\n    if (autoSubmit && code.length === codeLength && !isLoading) {\n      onSubmit?.(code);\n    }\n  }, [code, autoSubmit, isLoading, codeLength, onSubmit]);\n\n  const handleResend = useCallback(async () => {\n    if (cooldown > 0) return;\n    await onResend?.(deliveryMethod);\n    setCooldown(resendCooldown);\n  }, [cooldown, onResend, deliveryMethod, resendCooldown]);\n\n  const handleCodeChange = useCallback((newCode: string) => {\n    setCode(newCode);\n  }, []);\n\n  const codeError = errors?.code;\n  const generalError = errors?.general;\n  const methodConfig = DELIVERY_METHOD_CONFIG[deliveryMethod];\n  const MethodIcon = methodConfig.icon;\n  const formattedAddress = formatDeliveryAddress(\n    deliveryAddress,\n    deliveryMethod\n  );\n\n  return (\n    <Card className={cn(\"w-full max-w-sm shadow-xs\", className)}>\n      <CardHeader>\n        <CardTitle className=\"flex items-center gap-2\">\n          Verify your {methodConfig.label.toLowerCase()}\n        </CardTitle>\n        <CardDescription>\n          We&apos;ve sent a {codeLength}-digit code to{\" \"}\n          {deliveryAddress ? (\n            <span className=\"font-medium\">{formattedAddress}</span>\n          ) : (\n            \"your \" + methodConfig.label.toLowerCase()\n          )}\n        </CardDescription>\n      </CardHeader>\n      <CardContent>\n        <div className=\"flex flex-col gap-6\">\n          {generalError && <ErrorAlert message={generalError} />}\n\n          {availableMethods.length > 1 && onDeliveryMethodChange && (\n            <DeliveryMethodSelect\n              availableMethods={availableMethods}\n              deliveryMethod={deliveryMethod}\n              onDeliveryMethodChange={onDeliveryMethodChange}\n            />\n          )}\n\n          <OTPField\n            code={code}\n            codeError={codeError}\n            codeLength={codeLength}\n            isLoading={isLoading}\n            onCodeChange={handleCodeChange}\n            onResend={onResend ? handleResend : undefined}\n            resendCooldown={cooldown}\n          />\n\n          {!autoSubmit && (\n            <VerifyButton\n              code={code}\n              codeLength={codeLength}\n              isLoading={isLoading}\n              onSubmit={onSubmit || (() => {})}\n            />\n          )}\n        </div>\n      </CardContent>\n    </Card>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "block",
    "auth"
  ],
  "type": "registry:ui"
}