{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "auth-phone-verify",
  "title": "Auth Phone Verify",
  "description": "Verify phone number with OTP code via SMS.",
  "registryDependencies": [
    "button",
    "card",
    "field",
    "input-group",
    "input-otp",
    "select",
    "separator"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/auth/auth-phone-verify.tsx",
      "content": "\"use client\";\n\nimport {\n  CheckCircle2,\n  Loader2,\n  MessageSquare,\n  Phone,\n  RefreshCw,\n  XCircle,\n} 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  FieldError,\n  FieldLabel,\n} from \"@/registry/new-york/ui/field\";\nimport {\n  InputGroup,\n  InputGroupAddon,\n  InputGroupInput,\n} from \"@/registry/new-york/ui/input-group\";\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\";\nimport { Separator } from \"@/registry/new-york/ui/separator\";\n\nexport interface Country {\n  code: string;\n  name: string;\n  dialCode: string;\n  flag: string;\n}\n\nexport type VerificationStatus =\n  | \"pending\"\n  | \"sent\"\n  | \"verifying\"\n  | \"verified\"\n  | \"expired\"\n  | \"error\";\n\nexport interface AuthPhoneVerifyProps {\n  phoneNumber?: string;\n  countryCode?: string;\n  onPhoneSubmit?: (phoneNumber: string, countryCode: string) => void;\n  onOTPSubmit?: (code: string) => void;\n  onResend?: () => void;\n  onChangePhone?: () => void;\n  className?: string;\n  isLoading?: boolean;\n  status?: VerificationStatus;\n  resendCooldown?: number;\n  errors?: {\n    phone?: string;\n    code?: string;\n    general?: string;\n  };\n  autoSubmit?: boolean;\n  countries?: Country[];\n}\n\nconst DEFAULT_COUNTRIES: Country[] = [\n  { code: \"US\", name: \"United States\", dialCode: \"+1\", flag: \"🇺🇸\" },\n  { code: \"GB\", name: \"United Kingdom\", dialCode: \"+44\", flag: \"🇬🇧\" },\n  { code: \"CA\", name: \"Canada\", dialCode: \"+1\", flag: \"🇨🇦\" },\n  { code: \"AU\", name: \"Australia\", dialCode: \"+61\", flag: \"🇦🇺\" },\n  { code: \"DE\", name: \"Germany\", dialCode: \"+49\", flag: \"🇩🇪\" },\n  { code: \"FR\", name: \"France\", dialCode: \"+33\", flag: \"🇫🇷\" },\n  { code: \"IN\", name: \"India\", dialCode: \"+91\", flag: \"🇮🇳\" },\n  { code: \"JP\", name: \"Japan\", dialCode: \"+81\", flag: \"🇯🇵\" },\n  { code: \"CN\", name: \"China\", dialCode: \"+86\", flag: \"🇨🇳\" },\n  { code: \"BR\", name: \"Brazil\", dialCode: \"+55\", flag: \"🇧🇷\" },\n];\n\nfunction formatPhoneNumber(value: string, countryCode: string): string {\n  const digits = value.replace(/\\D/g, \"\");\n  if (countryCode === \"US\" || countryCode === \"CA\") {\n    if (digits.length <= 3) return digits;\n    if (digits.length <= 6) {\n      return `(${digits.slice(0, 3)}) ${digits.slice(3)}`;\n    }\n    return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6, 10)}`;\n  }\n  return digits;\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 VerifiedStateProps {\n  className?: string;\n  phoneNumber?: string;\n  selectedCountry?: Country;\n}\n\nfunction VerifiedState({\n  className,\n  phoneNumber,\n  selectedCountry,\n}: VerifiedStateProps) {\n  return (\n    <Card className={cn(\"w-full max-w-sm shadow-xs\", className)}>\n      <CardHeader>\n        <CardTitle>Phone verified</CardTitle>\n        <CardDescription>\n          Your phone number has been successfully verified\n        </CardDescription>\n      </CardHeader>\n      <CardContent>\n        <div className=\"flex flex-col items-center gap-4 rounded-lg border border-primary/20 bg-primary/5 p-6 text-center\">\n          <div className=\"flex size-16 items-center justify-center rounded-full bg-primary/10\">\n            <CheckCircle2 aria-hidden=\"true\" className=\"size-8 text-primary\" />\n          </div>\n          <div className=\"flex flex-col gap-2\">\n            <p className=\"font-medium text-sm\">\n              Phone number verified successfully\n            </p>\n            {phoneNumber && (\n              <p className=\"text-muted-foreground text-sm\">\n                {selectedCountry?.dialCode} {phoneNumber}\n              </p>\n            )}\n          </div>\n        </div>\n      </CardContent>\n    </Card>\n  );\n}\n\ninterface ExpiredErrorStateProps {\n  className?: string;\n  cooldown: number;\n  generalError?: string;\n  isLoading: boolean;\n  onChangePhone?: () => void;\n  onResend?: () => void;\n  status: \"expired\" | \"error\";\n}\n\nfunction ExpiredErrorState({\n  className,\n  cooldown,\n  generalError,\n  isLoading,\n  onChangePhone,\n  onResend,\n  status,\n}: ExpiredErrorStateProps) {\n  return (\n    <Card className={cn(\"w-full max-w-sm shadow-xs\", className)}>\n      <CardHeader>\n        <CardTitle>Verification failed</CardTitle>\n        <CardDescription>\n          {status === \"expired\"\n            ? \"The verification code has expired\"\n            : \"Something went wrong with verification\"}\n        </CardDescription>\n      </CardHeader>\n      <CardContent>\n        <div className=\"flex flex-col gap-4\">\n          {generalError && <ErrorAlert message={generalError} />}\n\n          <div className=\"flex flex-col items-center gap-4 rounded-lg border border-destructive/50 bg-destructive/10 p-6 text-center\">\n            <div className=\"flex size-16 items-center justify-center rounded-full bg-destructive/10\">\n              <XCircle aria-hidden=\"true\" className=\"size-8 text-destructive\" />\n            </div>\n            <p className=\"text-destructive text-sm\">\n              {status === \"expired\"\n                ? \"The verification code has expired. Please request a new one.\"\n                : \"Verification failed. Please try again.\"}\n            </p>\n          </div>\n\n          {onChangePhone && (\n            <Button\n              className=\"min-h-[44px] w-full touch-manipulation\"\n              onClick={onChangePhone}\n              type=\"button\"\n              variant=\"outline\"\n            >\n              Change phone number\n            </Button>\n          )}\n\n          {onResend && (\n            <Button\n              aria-busy={isLoading}\n              className=\"min-h-[44px] w-full touch-manipulation\"\n              data-loading={isLoading}\n              disabled={cooldown > 0 || isLoading}\n              onClick={onResend}\n              type=\"button\"\n            >\n              {isLoading ? (\n                <>\n                  <Loader2 aria-hidden=\"true\" className=\"size-4 animate-spin\" />\n                  Sending…\n                </>\n              ) : cooldown > 0 ? (\n                <>\n                  <RefreshCw aria-hidden=\"true\" className=\"size-4\" />\n                  Resend in {cooldown}s\n                </>\n              ) : (\n                <>\n                  <RefreshCw aria-hidden=\"true\" className=\"size-4\" />\n                  Request new code\n                </>\n              )}\n            </Button>\n          )}\n        </div>\n      </CardContent>\n    </Card>\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 OTPVerificationStateProps {\n  autoSubmit: boolean;\n  className?: string;\n  code: string;\n  codeError?: string;\n  cooldown: number;\n  generalError?: string;\n  isLoading: boolean;\n  onChangePhone?: () => void;\n  onCodeChange: (code: string) => void;\n  onOTPSubmit?: (code: string) => void;\n  onResend?: () => void;\n  phoneNumber?: string;\n  selectedCountry?: Country;\n  status: \"sent\" | \"verifying\";\n}\n\nfunction OTPVerificationState({\n  autoSubmit,\n  className,\n  code,\n  codeError,\n  cooldown,\n  generalError,\n  isLoading,\n  onChangePhone,\n  onCodeChange,\n  onOTPSubmit,\n  onResend,\n  phoneNumber,\n  selectedCountry,\n  status,\n}: OTPVerificationStateProps) {\n  return (\n    <Card className={cn(\"w-full max-w-sm shadow-xs\", className)}>\n      <CardHeader>\n        <CardTitle>Verify your phone number</CardTitle>\n        <CardDescription>\n          Enter the 6-digit code sent to{\" \"}\n          {phoneNumber && (\n            <span className=\"font-medium\">\n              {selectedCountry?.dialCode} {phoneNumber}\n            </span>\n          )}\n        </CardDescription>\n      </CardHeader>\n      <CardContent>\n        <div className=\"flex flex-col gap-6\">\n          {generalError && <ErrorAlert message={generalError} />}\n          <Field data-invalid={!!codeError}>\n            <FieldLabel htmlFor=\"phone-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={\n                  codeError ? \"phone-otp-code-error\" : undefined\n                }\n                aria-invalid={!!codeError}\n                disabled={isLoading || status === \"verifying\"}\n                id=\"phone-otp-code\"\n                maxLength={6}\n                onChange={onCodeChange}\n                value={code}\n              >\n                <InputOTPGroup>\n                  <InputOTPSlot index={0} />\n                  <InputOTPSlot index={1} />\n                  <InputOTPSlot index={2} />\n                  <InputOTPSlot index={3} />\n                  <InputOTPSlot index={4} />\n                  <InputOTPSlot index={5} />\n                </InputOTPGroup>\n              </InputOTP>\n              {codeError && (\n                <FieldError id=\"phone-otp-code-error\">{codeError}</FieldError>\n              )}\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 6-digit code from SMS</span>\n                {onResend && (\n                  <ResendButton\n                    cooldown={cooldown}\n                    isLoading={isLoading}\n                    onClick={onResend}\n                  />\n                )}\n              </div>\n            </FieldContent>\n          </Field>\n\n          {onChangePhone && (\n            <>\n              <Separator />\n              <Button\n                className=\"min-h-[44px] w-full touch-manipulation\"\n                onClick={onChangePhone}\n                type=\"button\"\n                variant=\"ghost\"\n              >\n                Change phone number\n              </Button>\n            </>\n          )}\n\n          {!autoSubmit && (\n            <Button\n              aria-busy={isLoading || status === \"verifying\"}\n              className=\"min-h-[44px] w-full touch-manipulation\"\n              data-loading={isLoading || status === \"verifying\"}\n              disabled={\n                isLoading || status === \"verifying\" || code.length !== 6\n              }\n              onClick={() => onOTPSubmit?.(code)}\n              type=\"button\"\n            >\n              {isLoading || status === \"verifying\" ? (\n                <>\n                  <Loader2 aria-hidden=\"true\" className=\"size-4 animate-spin\" />\n                  Verifying…\n                </>\n              ) : (\n                <>\n                  <CheckCircle2 aria-hidden=\"true\" className=\"size-4\" />\n                  Verify code\n                </>\n              )}\n            </Button>\n          )}\n        </div>\n      </CardContent>\n    </Card>\n  );\n}\n\ninterface CountrySelectProps {\n  countries: Country[];\n  countryCode: string;\n  onCountryCodeChange: (code: string) => void;\n}\n\nfunction CountrySelect({\n  countries,\n  countryCode,\n  onCountryCodeChange,\n}: CountrySelectProps) {\n  return (\n    <Field className=\"w-fit\">\n      <FieldLabel htmlFor=\"phone-country\">Country</FieldLabel>\n      <FieldContent>\n        <Select onValueChange={onCountryCodeChange} value={countryCode}>\n          <SelectTrigger id=\"phone-country\">\n            <SelectValue />\n          </SelectTrigger>\n          <SelectContent>\n            {countries.map((country) => (\n              <SelectItem key={country.code} value={country.code}>\n                <div className=\"flex items-center gap-2\">\n                  <span aria-hidden=\"true\">{country.flag}</span>\n                  <span>{country.dialCode}</span>\n                </div>\n              </SelectItem>\n            ))}\n          </SelectContent>\n        </Select>\n      </FieldContent>\n    </Field>\n  );\n}\n\ninterface PhoneNumberFieldProps {\n  error?: string;\n  id: string;\n  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;\n  value: string;\n}\n\nfunction PhoneNumberField({\n  error,\n  id,\n  onChange,\n  value,\n}: PhoneNumberFieldProps) {\n  return (\n    <Field className=\"flex-1\" data-invalid={!!error}>\n      <FieldLabel htmlFor={id}>\n        Phone number\n        <span aria-label=\"required\" className=\"text-destructive\">\n          *\n        </span>\n      </FieldLabel>\n      <FieldContent>\n        <InputGroup aria-invalid={!!error}>\n          <InputGroupAddon>\n            <Phone aria-hidden=\"true\" className=\"size-4\" />\n          </InputGroupAddon>\n          <InputGroupInput\n            aria-describedby={error ? `${id}-error` : undefined}\n            aria-invalid={!!error}\n            autoComplete=\"tel\"\n            id={id}\n            inputMode=\"tel\"\n            name=\"phoneNumber\"\n            onChange={onChange}\n            placeholder=\"(555) 123-4567…\"\n            required\n            type=\"tel\"\n            value={value}\n          />\n        </InputGroup>\n        {error && <FieldError id={`${id}-error`}>{error}</FieldError>}\n      </FieldContent>\n    </Field>\n  );\n}\n\ninterface PhoneNumberFormProps {\n  className?: string;\n  countries: Country[];\n  countryCode: string;\n  generalError?: string;\n  isLoading: boolean;\n  onCountryCodeChange: (code: string) => void;\n  onPhoneChange: (e: React.ChangeEvent<HTMLInputElement>) => void;\n  onPhoneSubmit: (e: React.FormEvent) => void;\n  phoneError?: string;\n  phoneNumber: string;\n}\n\nfunction PhoneNumberForm({\n  className,\n  countries,\n  countryCode,\n  generalError,\n  isLoading,\n  onCountryCodeChange,\n  onPhoneChange,\n  onPhoneSubmit,\n  phoneError,\n  phoneNumber,\n}: PhoneNumberFormProps) {\n  return (\n    <Card className={cn(\"w-full max-w-sm shadow-xs\", className)}>\n      <CardHeader>\n        <CardTitle>Verify your phone number</CardTitle>\n        <CardDescription>\n          We&apos;ll send a verification code via SMS\n        </CardDescription>\n      </CardHeader>\n      <CardContent>\n        <form className=\"flex flex-col gap-6\" onSubmit={onPhoneSubmit}>\n          {generalError && <ErrorAlert message={generalError} />}\n\n          <div className=\"flex flex-col gap-4\">\n            <div className=\"flex flex-col gap-2 sm:flex-row\">\n              <CountrySelect\n                countries={countries}\n                countryCode={countryCode}\n                onCountryCodeChange={onCountryCodeChange}\n              />\n              <PhoneNumberField\n                error={phoneError}\n                id=\"phone-number\"\n                onChange={onPhoneChange}\n                value={phoneNumber}\n              />\n            </div>\n          </div>\n          <p className=\"text-muted-foreground text-sm\">\n            We&apos;ll send a verification code to this number\n          </p>\n          <Button\n            aria-busy={isLoading}\n            className=\"min-h-[44px] w-full touch-manipulation\"\n            data-loading={isLoading}\n            disabled={isLoading || !phoneNumber.trim()}\n            type=\"submit\"\n          >\n            {isLoading ? (\n              <>\n                <Loader2 aria-hidden=\"true\" className=\"size-4 animate-spin\" />\n                Sending code…\n              </>\n            ) : (\n              <>\n                <MessageSquare aria-hidden=\"true\" className=\"size-4\" />\n                Send verification code\n              </>\n            )}\n          </Button>\n        </form>\n      </CardContent>\n    </Card>\n  );\n}\n\nexport default function AuthPhoneVerify({\n  phoneNumber: initialPhoneNumber,\n  countryCode: initialCountryCode = \"US\",\n  onPhoneSubmit,\n  onOTPSubmit,\n  onResend,\n  onChangePhone,\n  className,\n  isLoading = false,\n  status = \"pending\",\n  resendCooldown = 60,\n  errors,\n  autoSubmit = true,\n  countries = DEFAULT_COUNTRIES,\n}: AuthPhoneVerifyProps) {\n  const [phoneNumber, setPhoneNumber] = useState(initialPhoneNumber || \"\");\n  const [countryCode, setCountryCode] = useState(initialCountryCode);\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 === 6 && !isLoading && status === \"sent\") {\n      onOTPSubmit?.(code);\n    }\n  }, [code, autoSubmit, isLoading, status, onOTPSubmit]);\n\n  const handlePhoneSubmit = useCallback(\n    (e: React.FormEvent) => {\n      e.preventDefault();\n      if (phoneNumber.trim()) {\n        onPhoneSubmit?.(phoneNumber.trim(), countryCode);\n      }\n    },\n    [phoneNumber, countryCode, onPhoneSubmit]\n  );\n\n  const handleResend = useCallback(async () => {\n    if (cooldown > 0) return;\n    await onResend?.();\n    setCooldown(resendCooldown);\n  }, [cooldown, onResend, resendCooldown]);\n\n  const handlePhoneChange = useCallback(\n    (e: React.ChangeEvent<HTMLInputElement>) => {\n      const formatted = formatPhoneNumber(e.target.value, countryCode);\n      setPhoneNumber(formatted);\n    },\n    [countryCode]\n  );\n\n  const handleCodeChange = useCallback((newCode: string) => {\n    setCode(newCode);\n  }, []);\n\n  const selectedCountry = countries.find((c) => c.code === countryCode);\n  const phoneError = errors?.phone;\n  const codeError = errors?.code;\n  const generalError = errors?.general;\n\n  if (status === \"verified\") {\n    return (\n      <VerifiedState\n        className={className}\n        phoneNumber={phoneNumber}\n        selectedCountry={selectedCountry}\n      />\n    );\n  }\n\n  if (status === \"expired\" || status === \"error\") {\n    return (\n      <ExpiredErrorState\n        className={className}\n        cooldown={cooldown}\n        generalError={generalError}\n        isLoading={isLoading}\n        onChangePhone={onChangePhone}\n        onResend={onResend ? handleResend : undefined}\n        status={status}\n      />\n    );\n  }\n\n  if (status === \"sent\" || status === \"verifying\") {\n    return (\n      <OTPVerificationState\n        autoSubmit={autoSubmit}\n        className={className}\n        code={code}\n        codeError={codeError}\n        cooldown={cooldown}\n        generalError={generalError}\n        isLoading={isLoading}\n        onChangePhone={onChangePhone}\n        onCodeChange={handleCodeChange}\n        onOTPSubmit={onOTPSubmit}\n        onResend={onResend ? handleResend : undefined}\n        phoneNumber={phoneNumber}\n        selectedCountry={selectedCountry}\n        status={status}\n      />\n    );\n  }\n\n  return (\n    <PhoneNumberForm\n      className={className}\n      countries={countries}\n      countryCode={countryCode}\n      generalError={generalError}\n      isLoading={isLoading}\n      onCountryCodeChange={setCountryCode}\n      onPhoneChange={handlePhoneChange}\n      onPhoneSubmit={handlePhoneSubmit}\n      phoneError={phoneError}\n      phoneNumber={phoneNumber}\n    />\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "block",
    "auth"
  ],
  "type": "registry:ui"
}