{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "phone-input",
  "type": "registry:ui",
  "title": "Phone Input",
  "description": "An international phone field with a searchable country list, as-you-type formatting and paste-to-detect E.164 numbers.",
  "author": "WANSTUDIO <https://wanstudio.my>",
  "dependencies": [],
  "files": [
    {
      "path": "registry/wancraft/phone-input.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\n/* --------------------------------- dataset --------------------------------- */\n\nexport interface PhoneCountry {\n  /** ISO 3166-1 alpha-2 region code, e.g. \"MY\". */\n  code: string;\n  /** Country calling code without the \"+\", e.g. \"60\". */\n  dial: string;\n  /**\n   * National display formats where \"#\" is a digit. The first pattern with room for the typed digits\n   * is used, so list shorter formats first. Digits beyond the longest pattern are appended as typed.\n   */\n  patterns: string[];\n  /** National trunk prefix dropped when typed in front of a number (e.g. the \"0\" in \"07700 900123\"). */\n  trunk?: string;\n}\n\n/**\n * About thirty common regions with simplified national formats. This is a display aid, NOT a\n * numbering-plan database or a validation library: real plans vary by area and number type. Pass\n * your own `countries` (or use a library such as libphonenumber) when correctness matters.\n * Regions sharing a calling code are listed in priority order (US before CA for +1).\n */\nexport const PHONE_COUNTRIES: PhoneCountry[] = [\n  { code: \"US\", dial: \"1\", patterns: [\"(###) ###-####\"] },\n  { code: \"CA\", dial: \"1\", patterns: [\"(###) ###-####\"] },\n  { code: \"MX\", dial: \"52\", patterns: [\"## #### ####\"] },\n  { code: \"BR\", dial: \"55\", patterns: [\"## ####-####\", \"## #####-####\"] },\n  { code: \"AR\", dial: \"54\", patterns: [\"## ####-####\"] },\n  { code: \"GB\", dial: \"44\", patterns: [\"#### ######\"], trunk: \"0\" },\n  { code: \"IE\", dial: \"353\", patterns: [\"## ### ####\"], trunk: \"0\" },\n  { code: \"FR\", dial: \"33\", patterns: [\"# ## ## ## ##\"], trunk: \"0\" },\n  { code: \"DE\", dial: \"49\", patterns: [\"### #######\", \"#### #######\"], trunk: \"0\" },\n  { code: \"NL\", dial: \"31\", patterns: [\"# ########\"], trunk: \"0\" },\n  { code: \"BE\", dial: \"32\", patterns: [\"### ## ## ##\"], trunk: \"0\" },\n  { code: \"CH\", dial: \"41\", patterns: [\"## ### ## ##\"], trunk: \"0\" },\n  { code: \"ES\", dial: \"34\", patterns: [\"### ### ###\"] },\n  { code: \"PT\", dial: \"351\", patterns: [\"### ### ###\"] },\n  { code: \"IT\", dial: \"39\", patterns: [\"### ### ####\"] },\n  { code: \"SE\", dial: \"46\", patterns: [\"## ### ## ##\"], trunk: \"0\" },\n  { code: \"NO\", dial: \"47\", patterns: [\"### ## ###\"] },\n  { code: \"DK\", dial: \"45\", patterns: [\"## ## ## ##\"] },\n  { code: \"PL\", dial: \"48\", patterns: [\"### ### ###\"] },\n  { code: \"AE\", dial: \"971\", patterns: [\"## ### ####\"], trunk: \"0\" },\n  { code: \"SA\", dial: \"966\", patterns: [\"## ### ####\"], trunk: \"0\" },\n  { code: \"ZA\", dial: \"27\", patterns: [\"## ### ####\"], trunk: \"0\" },\n  { code: \"NG\", dial: \"234\", patterns: [\"### ### ####\"], trunk: \"0\" },\n  { code: \"IN\", dial: \"91\", patterns: [\"##### #####\"], trunk: \"0\" },\n  { code: \"MY\", dial: \"60\", patterns: [\"##-### ####\", \"##-#### ####\"], trunk: \"0\" },\n  { code: \"SG\", dial: \"65\", patterns: [\"#### ####\"] },\n  { code: \"ID\", dial: \"62\", patterns: [\"###-###-####\", \"###-####-####\"], trunk: \"0\" },\n  { code: \"TH\", dial: \"66\", patterns: [\"## ### ####\"], trunk: \"0\" },\n  { code: \"PH\", dial: \"63\", patterns: [\"### ### ####\"], trunk: \"0\" },\n  { code: \"VN\", dial: \"84\", patterns: [\"## ### ## ##\"], trunk: \"0\" },\n  { code: \"HK\", dial: \"852\", patterns: [\"#### ####\"] },\n  { code: \"CN\", dial: \"86\", patterns: [\"### #### ####\"], trunk: \"0\" },\n  { code: \"JP\", dial: \"81\", patterns: [\"## #### ####\"], trunk: \"0\" },\n  { code: \"KR\", dial: \"82\", patterns: [\"## #### ####\"], trunk: \"0\" },\n  { code: \"AU\", dial: \"61\", patterns: [\"### ### ###\"], trunk: \"0\" },\n  { code: \"NZ\", dial: \"64\", patterns: [\"## ### ####\"], trunk: \"0\" },\n];\n\n/* ---------------------------------- types ---------------------------------- */\n\nexport interface PhoneInputValue {\n  /** ISO region code of the selected country. */\n  country: string;\n  /** The national number as displayed (formatted when `formatAsYouType` is on). */\n  national: string;\n  /** \"+<dial><digits>\", or \"\" while no digits are entered. */\n  e164: string;\n  /** True once the digits reach the length of the country's shortest pattern. */\n  complete: boolean;\n}\n\nexport interface PhoneInputProps\n  extends Omit<React.ComponentPropsWithoutRef<\"div\">, \"onChange\" | \"defaultValue\" | \"children\"> {\n  /** Controlled number in E.164 (\"+60123456789\"). */\n  value?: string;\n  /** Initial number in E.164 when uncontrolled. */\n  defaultValue?: string;\n  /** Region selected initially when the number does not imply one. */\n  defaultCountry?: string;\n  /** Called after every edit with the country, formatted national number and E.164 string. */\n  onChange?: (value: PhoneInputValue) => void;\n  /** The region list. Defaults to `PHONE_COUNTRIES` (≈30 common regions, not exhaustive). */\n  countries?: PhoneCountry[];\n  /** Visible label, rendered as a real <label>. */\n  label?: string;\n  /** Helper text below the field (linked with aria-describedby). */\n  hint?: React.ReactNode;\n  /** External error message; overrides the built-in checks. */\n  error?: string;\n  /** BCP 47 locale for region names (Intl.DisplayNames) and sorting. */\n  locale?: string;\n  /** Insert spaces, dashes and brackets while typing. */\n  formatAsYouType?: boolean;\n  /** Show the calling code next to the region badge. */\n  showDialCode?: boolean;\n  /** Focus ring, caret and selection colour. */\n  accent?: string;\n  disabled?: boolean;\n  required?: boolean;\n  /** When set, a hidden input submits the E.164 value under this name. */\n  name?: string;\n  /** id of the text input (generated when omitted). */\n  inputId?: string;\n}\n\ninterface PhoneState {\n  country: string;\n  digits: string;\n}\n\n/* --------------------------------- helpers --------------------------------- */\n\nconst MAX_E164_DIGITS = 15;\n\nconst onlyDigits = (s: string) => s.replace(/\\D/g, \"\");\nconst countHashes = (pattern: string) => pattern.split(\"#\").length - 1;\n\nfunction findCountry(countries: PhoneCountry[], code: string) {\n  return countries.find((c) => c.code === code) ?? countries[0];\n}\n\nfunction maxDigits(country: PhoneCountry) {\n  return MAX_E164_DIGITS - country.dial.length;\n}\n\nfunction minDigits(country: PhoneCountry) {\n  return Math.min(...country.patterns.map(countHashes));\n}\n\n/** Formats national digits with the first pattern that fits; overflow digits are appended. */\nfunction formatNational(digits: string, country: PhoneCountry, enabled: boolean) {\n  if (!enabled || !digits) return digits;\n  const pattern = country.patterns.find((p) => countHashes(p) >= digits.length) ?? country.patterns[country.patterns.length - 1];\n  let out = \"\";\n  let i = 0;\n  for (const ch of pattern) {\n    if (i >= digits.length) break;\n    if (ch === \"#\") out += digits[i++];\n    else out += ch;\n  }\n  return out + digits.slice(i);\n}\n\n/** A believable placeholder from the first pattern: \"##-### ####\" → \"12-345 6789\". */\nfunction examplePlaceholder(country: PhoneCountry, enabled: boolean) {\n  let n = 1;\n  const digits = country.patterns[0].replace(/#/g, () => String(n++ % 10));\n  return enabled ? digits : onlyDigits(digits);\n}\n\n/** Strips one trunk prefix (\"0\") from the start of national digits. */\nfunction dropTrunk(digits: string, country: PhoneCountry) {\n  return country.trunk && digits.startsWith(country.trunk) ? digits.slice(country.trunk.length) : digits;\n}\n\ntype IntlParse = { kind: \"match\"; state: PhoneState } | { kind: \"pending\" } | { kind: \"unknown\" };\n\n/**\n * Resolves \"+<digits>\" against the calling codes. Codes are prefix-free (ITU E.164), so at most one\n * matches; regions sharing it prefer `preferred`, then list order.\n */\nfunction parseInternational(text: string, countries: PhoneCountry[], preferred: string): IntlParse {\n  const digits = onlyDigits(text);\n  const matches = countries.filter((c) => digits.startsWith(c.dial));\n  if (matches.length) {\n    const best = matches.reduce((a, b) => (b.dial.length > a.dial.length ? b : a));\n    const sameCode = matches.filter((c) => c.dial === best.dial);\n    const country = sameCode.find((c) => c.code === preferred) ?? sameCode[0];\n    const rest = dropTrunk(digits.slice(country.dial.length), country).slice(0, maxDigits(country));\n    return { kind: \"match\", state: { country: country.code, digits: rest } };\n  }\n  return countries.some((c) => c.dial.startsWith(digits)) ? { kind: \"pending\" } : { kind: \"unknown\" };\n}\n\nfunction initialState(e164: string | undefined, countries: PhoneCountry[], defaultCountry: string): PhoneState {\n  const fallback = findCountry(countries, defaultCountry).code;\n  if (e164) {\n    const parsed = parseInternational(e164, countries, fallback);\n    if (parsed.kind === \"match\") return parsed.state;\n  }\n  return { country: fallback, digits: \"\" };\n}\n\nfunction toE164(state: PhoneState, countries: PhoneCountry[]) {\n  return state.digits ? `+${findCountry(countries, state.country).dial}${state.digits}` : \"\";\n}\n\n/** Caret index in `display` just after the `n`th digit. */\nfunction caretAfterDigits(display: string, n: number) {\n  if (n <= 0) {\n    const first = display.search(/\\d/);\n    return first === -1 ? display.length : Math.min(first, display.length);\n  }\n  let seen = 0;\n  for (let i = 0; i < display.length; i++) {\n    if (/\\d/.test(display[i]) && ++seen === n) return i + 1;\n  }\n  return display.length;\n}\n\nfunction regionNamer(locale: string) {\n  let names: Intl.DisplayNames | null = null;\n  try {\n    names = new Intl.DisplayNames([locale], { type: \"region\" });\n  } catch {\n    names = null;\n  }\n  // Collapse exotic spaces so server and browser ICU builds render the same text.\n  return (code: string) => (names?.of(code) ?? code).replace(/[  ]/g, \" \");\n}\n\n/** Region badge: the ISO code in a small mono frame (no emoji flags, no images). */\nfunction RegionBadge({ code, active = false }: { code: string; active?: boolean }) {\n  return (\n    <span\n      aria-hidden=\"true\"\n      className={`inline-flex h-[18px] min-w-[26px] shrink-0 items-center justify-center rounded-[4px] border px-1 font-mono text-[10px] font-semibold leading-none tracking-[0.06em] ${\n        active\n          ? \"border-[var(--pi-accent)] bg-[var(--pi-accent)] text-zinc-950\"\n          : \"border-zinc-900/15 bg-zinc-900/[0.03] text-zinc-700 dark:border-white/15 dark:bg-white/[0.05] dark:text-zinc-200\"\n      }`}\n    >\n      {code}\n    </span>\n  );\n}\n\n/* -------------------------------- component -------------------------------- */\n\n/**\n * An international phone field: a searchable country listbox (ARIA combobox) beside a tel input\n * that formats as you type while keeping the caret on the same digit. Typing, pasting or\n * autofilling a \"+\" number selects its country. Reports { country, national, e164 } on change.\n */\nexport function PhoneInput({\n  value,\n  defaultValue,\n  defaultCountry = \"MY\",\n  onChange,\n  countries = PHONE_COUNTRIES,\n  label = \"Phone number\",\n  hint,\n  error,\n  locale = \"en\",\n  formatAsYouType = true,\n  showDialCode = true,\n  accent = \"#FF7448\",\n  disabled = false,\n  required = false,\n  name,\n  inputId: inputIdProp,\n  className = \"\",\n  style,\n  ...props\n}: PhoneInputProps) {\n  const uid = React.useId();\n  const inputId = inputIdProp ?? `pi-${uid}`;\n  const hintId = `${inputId}-hint`;\n  const errorId = `${inputId}-error`;\n  const popoverId = `${inputId}-countries`;\n  const listboxId = `${inputId}-listbox`;\n  const optionId = (code: string) => `${inputId}-opt-${code}`;\n\n  const [state, setState] = React.useState<PhoneState>(() => initialState(value ?? defaultValue, countries, defaultCountry));\n  /** Raw text while a \"+\" number is being typed and its calling code is not resolved yet. */\n  const [draft, setDraft] = React.useState<string | null>(null);\n  const [draftError, setDraftError] = React.useState<string | null>(null);\n  const [touched, setTouched] = React.useState(false);\n\n  // Controlled mode: adopt an outside value that differs from what this field last produced.\n  const [syncedValue, setSyncedValue] = React.useState(value);\n  if (value !== undefined && value !== syncedValue) {\n    setSyncedValue(value);\n    if (value !== toE164(state, countries)) {\n      setState(initialState(value, countries, state.country));\n      setDraft(null);\n      setDraftError(null);\n    }\n  }\n\n  const [open, setOpen] = React.useState(false);\n  const [query, setQuery] = React.useState(\"\");\n  const [activeCode, setActiveCode] = React.useState<string | null>(null);\n  const [announcement, setAnnouncement] = React.useState(\"\");\n\n  const inputRef = React.useRef<HTMLInputElement>(null);\n  const fieldRef = React.useRef<HTMLDivElement>(null);\n  const triggerRef = React.useRef<HTMLButtonElement>(null);\n  const popoverRef = React.useRef<HTMLDivElement>(null);\n  const searchRef = React.useRef<HTMLInputElement>(null);\n  const listRef = React.useRef<HTMLDivElement>(null);\n  /** Digits that should sit before the caret after the next render (set by edit handlers). */\n  const pendingCaret = React.useRef<number | null>(null);\n  /** Where focus goes when the country popover closes. */\n  const focusAfterClose = React.useRef<\"trigger\" | \"input\">(\"trigger\");\n\n  const country = findCountry(countries, state.country);\n  const nameOf = regionNamer(locale);\n  const countryName = nameOf(country.code);\n  const national = formatNational(state.digits, country, formatAsYouType);\n  const display = draft ?? national;\n  const e164 = toE164(state, countries);\n  const complete = state.digits.length >= minDigits(country);\n\n  const builtInError =\n    draftError ?? (touched && state.digits.length > 0 && !complete ? `Enter a complete number for ${countryName}.` : null);\n  const errorText = error ?? builtInError;\n  const describedBy = [hint ? hintId : null, errorText ? errorId : null].filter(Boolean).join(\" \") || undefined;\n\n  /* --------------------------------- commits -------------------------------- */\n\n  const commit = (next: PhoneState) => {\n    setState(next);\n    setDraft(null);\n    setDraftError(null);\n    const c = findCountry(countries, next.country);\n    onChange?.({\n      country: c.code,\n      national: formatNational(next.digits, c, formatAsYouType),\n      e164: toE164(next, countries),\n      complete: next.digits.length >= minDigits(c),\n    });\n  };\n\n  const announce = (message: string) => setAnnouncement((prev) => (prev === message ? `${message} ` : message));\n\n  // Restore the caret onto the same digit after formatting re-renders the text.\n  React.useLayoutEffect(() => {\n    const input = inputRef.current;\n    const n = pendingCaret.current;\n    if (!input || n === null) return;\n    pendingCaret.current = null;\n    if (document.activeElement !== input) return;\n    const at = caretAfterDigits(input.value, n);\n    input.setSelectionRange(at, at);\n  });\n\n  const handleInput = (event: React.ChangeEvent<HTMLInputElement>) => {\n    const raw = event.target.value;\n    const caret = event.target.selectionStart ?? raw.length;\n\n    // International entry: \"+…\" typed, pasted or autofilled (autocomplete=\"tel\" gives \"+60 12-…\").\n    if (raw.trimStart().startsWith(\"+\")) {\n      const parsed = parseInternational(raw, countries, state.country);\n      if (parsed.kind === \"match\") {\n        const next = parsed.state;\n        const c = findCountry(countries, next.country);\n        const before = onlyDigits(raw.slice(0, caret)).length - c.dial.length;\n        pendingCaret.current = Math.max(0, Math.min(next.digits.length, before));\n        if (next.country !== state.country) announce(`${nameOf(c.code)}, +${c.dial}, selected from the number.`);\n        commit(next);\n      } else {\n        setDraft(raw.replace(/[^\\d+\\s()-]/g, \"\"));\n        setDraftError(parsed.kind === \"unknown\" ? \"Unknown country calling code.\" : null);\n      }\n      return;\n    }\n\n    let digits = onlyDigits(raw);\n    let before = onlyDigits(raw.slice(0, caret)).length;\n    const trimmed = dropTrunk(digits, country);\n    if (trimmed.length !== digits.length) {\n      before = Math.max(0, before - (digits.length - trimmed.length));\n      digits = trimmed;\n    }\n    digits = digits.slice(0, maxDigits(country));\n    pendingCaret.current = Math.min(before, digits.length);\n    commit({ country: state.country, digits });\n  };\n\n  const removeDigitAt = (index: number) => {\n    const digits = state.digits.slice(0, index) + state.digits.slice(index + 1);\n    pendingCaret.current = index;\n    commit({ country: state.country, digits });\n  };\n\n  const handleInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {\n    if (draft !== null || !formatAsYouType) return;\n    const input = event.currentTarget;\n    const start = input.selectionStart ?? 0;\n    if (start !== input.selectionEnd) return;\n    // Deleting a separator would be undone by formatting, so delete the neighbouring digit instead.\n    if (event.key === \"Backspace\" && start > 0 && !/\\d/.test(input.value[start - 1])) {\n      const n = onlyDigits(input.value.slice(0, start)).length;\n      if (n > 0) {\n        event.preventDefault();\n        removeDigitAt(n - 1);\n      }\n    } else if (event.key === \"Delete\" && start < input.value.length && !/\\d/.test(input.value[start])) {\n      const n = onlyDigits(input.value.slice(0, start)).length;\n      if (n < state.digits.length) {\n        event.preventDefault();\n        removeDigitAt(n);\n      }\n    } else if (event.key === \"ArrowDown\" && event.altKey) {\n      event.preventDefault();\n      popoverRef.current?.showPopover();\n    }\n  };\n\n  /* ------------------------------ country list ------------------------------ */\n\n  const collator = new Intl.Collator(locale);\n  const q = query.trim().toLowerCase().replace(/^\\+/, \"\");\n  const sorted = [...countries].sort((a, b) => collator.compare(nameOf(a.code), nameOf(b.code)));\n  const filtered = q\n    ? sorted.filter((c) => nameOf(c.code).toLowerCase().includes(q) || c.code.toLowerCase() === q || c.dial.startsWith(q))\n    : sorted;\n  const active = filtered.find((c) => c.code === activeCode) ?? filtered[0] ?? null;\n\n  const scrollToOption = (code: string) => {\n    const list = listRef.current;\n    const el = list?.querySelector<HTMLElement>(`#${CSS.escape(optionId(code))}`);\n    if (!list || !el) return;\n    // Scroll the listbox only — scrollIntoView would move the page too.\n    if (el.offsetTop < list.scrollTop) list.scrollTop = el.offsetTop - 4;\n    else if (el.offsetTop + el.offsetHeight > list.scrollTop + list.clientHeight)\n      list.scrollTop = el.offsetTop + el.offsetHeight - list.clientHeight + 4;\n  };\n\n  const setActive = (code: string) => {\n    setActiveCode(code);\n    requestAnimationFrame(() => scrollToOption(code));\n  };\n\n  const closePopover = () => {\n    const pop = popoverRef.current;\n    if (pop?.matches(\":popover-open\")) pop.hidePopover();\n  };\n\n  const chooseCountry = (code: string) => {\n    const c = findCountry(countries, code);\n    const digits = dropTrunk(state.digits, c).slice(0, maxDigits(c));\n    pendingCaret.current = digits.length;\n    commit({ country: c.code, digits });\n    announce(`${nameOf(c.code)}, +${c.dial}, selected.`);\n    focusAfterClose.current = \"input\";\n    closePopover();\n  };\n\n  const position = React.useCallback(() => {\n    const pop = popoverRef.current;\n    const field = fieldRef.current;\n    if (!pop || !field) return;\n    const rect = field.getBoundingClientRect();\n    const height = pop.offsetHeight;\n    const width = pop.offsetWidth;\n    const below = rect.bottom + 6;\n    const top = below + height > window.innerHeight - 8 && rect.top - height - 6 > 8 ? rect.top - height - 6 : below;\n    pop.style.top = `${Math.round(top)}px`;\n    pop.style.left = `${Math.round(Math.min(Math.max(rect.left, 8), Math.max(window.innerWidth - width - 8, 8)))}px`;\n  }, []);\n\n  React.useEffect(() => {\n    const pop = popoverRef.current;\n    if (!pop) return;\n    const onToggle = (event: Event) => {\n      const next = (event as ToggleEvent).newState === \"open\";\n      setOpen(next);\n      if (next) {\n        focusAfterClose.current = \"trigger\";\n        setQuery(\"\");\n        setActiveCode(state.country);\n        requestAnimationFrame(() => {\n          searchRef.current?.focus({ preventScroll: true });\n          scrollToOption(state.country);\n        });\n      } else if (focusAfterClose.current === \"input\") {\n        inputRef.current?.focus({ preventScroll: true });\n      } else if (pop.contains(document.activeElement) || document.activeElement === document.body) {\n        triggerRef.current?.focus({ preventScroll: true });\n      }\n    };\n    pop.addEventListener(\"toggle\", onToggle);\n    return () => pop.removeEventListener(\"toggle\", onToggle);\n  });\n\n  // Keep the popover attached to the field while it is open.\n  React.useEffect(() => {\n    if (!open) return;\n    position();\n    window.addEventListener(\"scroll\", position, { capture: true, passive: true });\n    window.addEventListener(\"resize\", position);\n    return () => {\n      window.removeEventListener(\"scroll\", position, { capture: true });\n      window.removeEventListener(\"resize\", position);\n    };\n  }, [open, position]);\n\n  const handleSearchKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {\n    const index = active ? filtered.indexOf(active) : -1;\n    const move = (to: number) => {\n      const c = filtered[Math.max(0, Math.min(filtered.length - 1, to))];\n      if (c) setActive(c.code);\n    };\n    switch (event.key) {\n      case \"ArrowDown\":\n        event.preventDefault();\n        move(index + 1);\n        break;\n      case \"ArrowUp\":\n        event.preventDefault();\n        move(index - 1);\n        break;\n      case \"PageDown\":\n        event.preventDefault();\n        move(index + 6);\n        break;\n      case \"PageUp\":\n        event.preventDefault();\n        move(index - 6);\n        break;\n      case \"Home\":\n        event.preventDefault();\n        move(0);\n        break;\n      case \"End\":\n        event.preventDefault();\n        move(filtered.length - 1);\n        break;\n      case \"Enter\":\n        event.preventDefault();\n        if (active) chooseCountry(active.code);\n        break;\n      case \"Escape\":\n        // Close only this layer; keep the page (and any surrounding dialog) untouched.\n        event.preventDefault();\n        event.stopPropagation();\n        closePopover();\n        break;\n      case \"Tab\":\n        closePopover();\n        break;\n    }\n  };\n\n  const rootStyle = { [\"--pi-accent\" as string]: accent, ...style };\n\n  return (\n    <div className={`flex w-full max-w-sm flex-col gap-1.5 ${className}`} style={rootStyle} {...props}>\n      <label htmlFor={inputId} className=\"font-mono text-[10px] uppercase tracking-[0.14em] text-zinc-500 dark:text-zinc-400\">\n        {label}\n        {required ? <span aria-hidden=\"true\"> *</span> : null}\n      </label>\n\n      <div\n        ref={fieldRef}\n        role=\"group\"\n        aria-label={label}\n        data-invalid={errorText ? \"\" : undefined}\n        className={`group/pi flex h-11 items-stretch rounded-lg border bg-white transition-[border-color,box-shadow] duration-150 dark:bg-white/[0.04] ${\n          errorText\n            ? \"border-red-500/70 dark:border-red-400/70\"\n            : \"border-zinc-900/15 focus-within:border-[var(--pi-accent)] dark:border-white/15 dark:focus-within:border-[var(--pi-accent)]\"\n        } focus-within:shadow-[0_0_0_3px_color-mix(in_oklab,var(--pi-accent)_22%,transparent)] ${disabled ? \"opacity-50\" : \"\"}`}\n      >\n        <button\n          ref={triggerRef}\n          type=\"button\"\n          disabled={disabled}\n          popoverTarget={popoverId}\n          aria-haspopup=\"listbox\"\n          aria-expanded={open}\n          aria-controls={popoverId}\n          aria-label={`Country: ${countryName}, +${country.dial}. Change country`}\n          className=\"flex shrink-0 items-center gap-1.5 rounded-l-[7px] border-r border-zinc-900/10 pl-2.5 pr-2 text-zinc-800 outline-hidden transition-colors hover:bg-zinc-900/[0.03] focus-visible:bg-zinc-900/[0.04] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-solid focus-visible:outline-[var(--pi-accent)] disabled:cursor-not-allowed dark:border-white/10 dark:text-zinc-100 dark:hover:bg-white/[0.04] dark:focus-visible:bg-white/[0.06]\"\n        >\n          <RegionBadge code={country.code} />\n          {showDialCode ? <span className=\"font-mono text-[13px] tabular-nums tracking-[-0.01em]\">+{country.dial}</span> : null}\n          <svg\n            viewBox=\"0 0 12 12\"\n            aria-hidden=\"true\"\n            className={`size-3 text-zinc-400 transition-transform duration-150 motion-reduce:transition-none dark:text-zinc-500 ${open ? \"rotate-180\" : \"\"}`}\n          >\n            <path d=\"M3 4.5 6 7.5l3-3\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.4\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n          </svg>\n        </button>\n\n        <input\n          ref={inputRef}\n          id={inputId}\n          type=\"tel\"\n          inputMode=\"tel\"\n          autoComplete=\"tel\"\n          spellCheck={false}\n          disabled={disabled}\n          required={required}\n          placeholder={examplePlaceholder(country, formatAsYouType)}\n          value={display}\n          aria-invalid={errorText ? true : undefined}\n          aria-describedby={describedBy}\n          onChange={handleInput}\n          onKeyDown={handleInputKeyDown}\n          onBlur={() => setTouched(true)}\n          className=\"min-w-0 flex-1 bg-transparent px-3 font-mono text-[14px] tabular-nums tracking-[0.01em] text-zinc-900 caret-[var(--pi-accent)] outline-hidden selection:bg-[color-mix(in_oklab,var(--pi-accent)_30%,transparent)] placeholder:text-zinc-400 disabled:cursor-not-allowed dark:text-zinc-50 dark:placeholder:text-zinc-500\"\n        />\n\n        <span\n          aria-hidden=\"true\"\n          className={`flex items-center pr-3 transition-opacity duration-150 motion-reduce:transition-none ${complete && !errorText ? \"opacity-100\" : \"opacity-0\"}`}\n        >\n          <svg viewBox=\"0 0 16 16\" className=\"size-4 text-[var(--pi-accent)]\">\n            <path d=\"m3.5 8.5 3 3 6-7\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n          </svg>\n        </span>\n      </div>\n\n      <div className=\"flex min-h-4 flex-col font-mono text-[10px] tracking-[0.04em]\">\n        {hint ? (\n          <p id={hintId} className={errorText ? \"sr-only\" : \"text-zinc-500 dark:text-zinc-400\"}>\n            {hint}\n          </p>\n        ) : null}\n        <p id={errorId} aria-live=\"polite\" className=\"text-red-600 dark:text-red-400\">\n          {errorText ?? \"\"}\n        </p>\n      </div>\n\n      <div\n        ref={popoverRef}\n        id={popoverId}\n        popover=\"auto\"\n        className=\"fixed inset-auto m-0 w-[17.5rem] overflow-hidden rounded-xl border border-zinc-900/10 bg-white p-0 text-zinc-900 shadow-[0_24px_48px_-24px_rgb(0_0_0/0.35)] dark:border-white/10 dark:bg-zinc-900 dark:text-zinc-100\"\n        style={rootStyle}\n      >\n        {open ? (\n          <>\n            <div className=\"flex items-center gap-2 border-b border-zinc-900/10 px-3 dark:border-white/10\">\n              <svg viewBox=\"0 0 16 16\" aria-hidden=\"true\" className=\"size-3.5 shrink-0 text-zinc-400\">\n                <circle cx=\"7\" cy=\"7\" r=\"4.5\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.4\" />\n                <path d=\"m10.5 10.5 3 3\" stroke=\"currentColor\" strokeWidth=\"1.4\" strokeLinecap=\"round\" />\n              </svg>\n              <input\n                ref={searchRef}\n                type=\"text\"\n                role=\"combobox\"\n                aria-expanded=\"true\"\n                aria-controls={listboxId}\n                aria-autocomplete=\"list\"\n                aria-activedescendant={active ? optionId(active.code) : undefined}\n                aria-label=\"Search countries\"\n                placeholder=\"Search country or +code\"\n                autoComplete=\"off\"\n                spellCheck={false}\n                value={query}\n                onChange={(event) => {\n                  setQuery(event.target.value);\n                  setActiveCode(null);\n                  if (listRef.current) listRef.current.scrollTop = 0;\n                }}\n                onKeyDown={handleSearchKeyDown}\n                className=\"h-10 min-w-0 flex-1 bg-transparent text-[13px] text-zinc-900 outline-hidden placeholder:text-zinc-400 dark:text-zinc-50 dark:placeholder:text-zinc-500\"\n              />\n            </div>\n            <div\n              ref={listRef}\n              id={listboxId}\n              role=\"listbox\"\n              aria-label=\"Countries\"\n              className=\"max-h-64 overflow-y-auto overscroll-contain p-1\"\n            >\n              {filtered.map((c) => {\n                const selected = c.code === state.country;\n                const isActive = c.code === active?.code;\n                return (\n                  <div\n                    key={c.code}\n                    id={optionId(c.code)}\n                    role=\"option\"\n                    aria-selected={selected}\n                    onPointerDown={(event) => event.preventDefault()}\n                    onPointerEnter={() => setActiveCode(c.code)}\n                    onClick={() => chooseCountry(c.code)}\n                    className={`flex cursor-pointer items-center gap-2.5 rounded-md px-2 py-1.5 text-[13px] ${\n                      isActive ? \"bg-zinc-900/[0.05] dark:bg-white/[0.07]\" : \"\"\n                    }`}\n                  >\n                    <RegionBadge code={c.code} active={selected} />\n                    <span className=\"min-w-0 flex-1 truncate\">{nameOf(c.code)}</span>\n                    <span className=\"font-mono text-[11px] tabular-nums text-zinc-500 dark:text-zinc-400\">+{c.dial}</span>\n                  </div>\n                );\n              })}\n              {filtered.length === 0 ? (\n                <p className=\"px-2 py-6 text-center font-mono text-[10px] uppercase tracking-[0.14em] text-zinc-500 dark:text-zinc-400\">\n                  No matching country\n                </p>\n              ) : null}\n            </div>\n          </>\n        ) : null}\n      </div>\n\n      {name ? <input type=\"hidden\" name={name} value={e164} /> : null}\n      <span className=\"sr-only\" aria-live=\"polite\">\n        {announcement}\n      </span>\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "forms"
  ],
  "docs": "Documentation: https://wancraft.my/components/phone-input"
}
