{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "phone-input",
  "title": "Phone Input",
  "author": "Shadi Al Milhem",
  "description": "A design-system-native international phone input with compact country search, inferred flags, as-you-type formatting, E.164 output, and validity metadata.",
  "dependencies": [
    "libphonenumber-js"
  ],
  "registryDependencies": [
    "input",
    "@flagcn/country-picker"
  ],
  "files": [
    {
      "path": "src/components/flags/phone-input.tsx",
      "content": "// SPDX-License-Identifier: MIT\n\nimport {\n  AsYouType,\n  isPossiblePhoneNumber,\n  isValidPhoneNumber,\n  parsePhoneNumberFromString,\n  type CountryCode as LibPhoneCountryCode,\n} from \"libphonenumber-js/max\"\nimport * as React from \"react\"\n\nimport { Input } from \"@/components/ui/input\"\nimport { cn } from \"@/lib/utils\"\n\nimport { CountryPicker } from \"./country-picker\"\nimport { isPhoneCountryCode } from \"./country-utils\"\nimport { phoneCountryCodes, type PhoneCountryCode } from \"./data/countries\"\n\nexport interface PhoneValueMeta {\n  country: PhoneCountryCode\n  formatted: string\n  e164?: string\n  nationalNumber?: string\n  countryCallingCode?: string\n  international?: string\n  possible: boolean\n  valid: boolean\n}\n\nexport interface PhoneInputProps extends Omit<React.ComponentProps<typeof Input>, \"value\" | \"defaultValue\" | \"onChange\" | \"size\"> {\n  value?: string\n  defaultValue?: string\n  country?: PhoneCountryCode\n  defaultCountry?: PhoneCountryCode\n  onCountryChange?: (country: PhoneCountryCode) => void\n  onValueChange?: (value: string, meta: PhoneValueMeta) => void\n  countries?: readonly PhoneCountryCode[]\n  countryPickerLabel?: string\n  displayFormat?: \"national\" | \"international\"\n  size?: \"sm\" | \"default\" | \"lg\"\n  inputClassName?: string\n}\n\nfunction phoneCountry(code: PhoneCountryCode) {\n  return code.toUpperCase() as LibPhoneCountryCode\n}\n\nfunction normalizeInternationalPrefix(value: string) {\n  return value.trimStart().startsWith(\"00\") ? value.replace(/^\\s*00/, \"+\") : value\n}\n\nfunction parsePhone(value: string, country: PhoneCountryCode) {\n  return parsePhoneNumberFromString(normalizeInternationalPrefix(value), phoneCountry(country))\n}\n\nfunction inferCountry(value: string) {\n  const parsed = parsePhoneNumberFromString(normalizeInternationalPrefix(value))\n  const inferred = parsed?.country?.toLocaleLowerCase()\n  return inferred && isPhoneCountryCode(inferred) ? inferred : undefined\n}\n\nfunction phoneMeta(value: string, country: PhoneCountryCode): PhoneValueMeta {\n  const parsed = parsePhone(value, country)\n  return {\n    country,\n    formatted: value,\n    e164: parsed?.number,\n    nationalNumber: parsed?.nationalNumber,\n    countryCallingCode: parsed?.countryCallingCode ? `+${parsed.countryCallingCode}` : undefined,\n    international: parsed?.formatInternational(),\n    possible: value.length > 0 && isPossiblePhoneNumber(value, phoneCountry(country)),\n    valid: value.length > 0 && isValidPhoneNumber(value, phoneCountry(country)),\n  }\n}\n\nfunction displayValue(value: string, country: PhoneCountryCode, format: \"national\" | \"international\") {\n  if (!value) return value\n  const parsed = parsePhone(value, country)\n  if (!parsed) return value\n  return format === \"international\" ? parsed.formatInternational() : parsed.formatNational()\n}\n\nexport const PhoneInput = React.forwardRef<HTMLInputElement, PhoneInputProps>(function PhoneInput({\n  value,\n  defaultValue = \"\",\n  country,\n  defaultCountry = \"us\",\n  onCountryChange,\n  onValueChange,\n  countries = phoneCountryCodes,\n  countryPickerLabel = \"Country calling code\",\n  displayFormat = \"national\",\n  size = \"default\",\n  className,\n  inputClassName,\n  disabled,\n  readOnly,\n  \"aria-label\": ariaLabel,\n  \"aria-invalid\": ariaInvalid,\n  \"aria-describedby\": ariaDescribedBy,\n  \"aria-errormessage\": ariaErrorMessage,\n  ...props\n}, ref) {\n  const initialCountry = inferCountry(defaultValue) ?? defaultCountry\n  const [internalValue, setInternalValue] = React.useState(() => displayValue(defaultValue, initialCountry, displayFormat))\n  const [internalCountry, setInternalCountry] = React.useState<PhoneCountryCode>(initialCountry)\n  const inferredControlledCountry = value ? inferCountry(value) : undefined\n  const currentCountry = country ?? inferredControlledCountry ?? internalCountry\n  const currentValue = value === undefined ? internalValue : displayValue(value, currentCountry, displayFormat)\n\n  function updateCountry(nextCountry: PhoneCountryCode) {\n    if (country === undefined) setInternalCountry(nextCountry)\n    onCountryChange?.(nextCountry)\n\n    if (!currentValue) return\n    const nextDisplay = displayValue(currentValue, nextCountry, displayFormat)\n    if (value === undefined) setInternalValue(nextDisplay)\n    const meta = phoneMeta(nextDisplay, nextCountry)\n    onValueChange?.(meta.e164 ?? nextDisplay, meta)\n  }\n\n  function updateValue(raw: string) {\n    const normalized = normalizeInternationalPrefix(raw)\n    const formatter = new AsYouType(normalized.startsWith(\"+\") ? undefined : phoneCountry(currentCountry))\n    const formatted = formatter.input(normalized)\n    const inferred = formatter.getCountry()?.toLocaleLowerCase()\n    const nextCountry = inferred && isPhoneCountryCode(inferred) ? inferred : currentCountry\n\n    if (value === undefined) setInternalValue(formatted)\n    if (country === undefined && nextCountry !== internalCountry) {\n      setInternalCountry(nextCountry)\n      onCountryChange?.(nextCountry)\n    }\n\n    const meta = phoneMeta(formatted, nextCountry)\n    onValueChange?.(meta.e164 ?? formatted, meta)\n  }\n\n  return (\n    <div\n      data-slot=\"phone-input\"\n      data-size={size}\n      data-invalid={ariaInvalid || undefined}\n      className={cn(\n        \"border-input bg-background flex w-full rounded-md border shadow-xs transition-[color,box-shadow]\",\n        \"focus-within:border-ring focus-within:ring-ring/30 focus-within:ring-[3px]\",\n        \"has-[[aria-invalid=true]]:border-destructive has-[[aria-invalid=true]]:ring-destructive/20\",\n        disabled && \"cursor-not-allowed opacity-50\",\n        className,\n      )}\n    >\n      <CountryPicker\n        value={currentCountry}\n        countries={countries}\n        onValueChange={(nextCountry) => {\n          if (isPhoneCountryCode(nextCountry)) updateCountry(nextCountry)\n        }}\n        showCallingCode\n        showCode={false}\n        triggerDisplay=\"calling-code\"\n        aria-label={countryPickerLabel}\n        disabled={disabled || readOnly}\n        className=\"order-first w-auto shrink-0\"\n        triggerClassName={cn(\n          \"h-full min-w-[5.25rem] rounded-e-none border-0 border-e px-2.5 shadow-none focus-visible:z-10\",\n          size === \"sm\" && \"h-8 min-w-[4.75rem] px-2 text-xs sm:h-8\",\n          size === \"default\" && \"h-9 sm:h-9\",\n          size === \"lg\" && \"h-10 sm:h-10\",\n        )}\n        contentClassName=\"w-[min(21rem,calc(100vw-2rem))]\"\n      />\n      <Input\n        ref={ref}\n        type=\"tel\"\n        inputMode=\"tel\"\n        autoComplete=\"tel\"\n        value={currentValue}\n        onChange={(event) => updateValue(event.target.value)}\n        disabled={disabled}\n        readOnly={readOnly}\n        aria-label={ariaLabel}\n        aria-invalid={ariaInvalid}\n        aria-describedby={ariaDescribedBy}\n        aria-errormessage={ariaErrorMessage}\n        className={cn(\n          \"rounded-s-none border-0 bg-transparent shadow-none focus-visible:ring-0\",\n          size === \"sm\" && \"h-8 px-2.5 text-xs md:text-xs\",\n          size === \"default\" && \"h-9\",\n          size === \"lg\" && \"h-10 px-4 text-base\",\n          inputClassName,\n        )}\n        {...props}\n      />\n    </div>\n  )\n})\n",
      "type": "registry:component",
      "target": "@components/flags/phone-input.tsx"
    }
  ],
  "meta": {
    "license": "MIT",
    "e164": true,
    "asYouType": true,
    "countryInference": true,
    "sizeVariants": [
      "sm",
      "default",
      "lg"
    ]
  },
  "docs": "onValueChange receives E.164 when parseable plus formatted, possible, valid, calling-code, and international metadata. Controlled E.164 values infer their country.",
  "categories": [
    "countries",
    "phone",
    "forms"
  ],
  "type": "registry:block"
}