{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "author": "DesignX ", "name": "country-select", "type": "registry:component", "title": "Country Select", "description": "A region picker dialog with flags, search and locale detection.", "dependencies": [ "lucide-react@^0.577.0" ], "registryDependencies": [ "@dx/dialog", "@dx/utils" ], "files": [ { "path": "dx/country-select.tsx", "type": "registry:component", "target": "components/dx/country-select.tsx", "content": "import * as React from \"react\";\nimport { CheckIcon, ChevronDownIcon, SearchIcon } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from \"@/components/ui/dialog\";\n\n/**\n * CountrySelecta flag pill that opens a searchable region dialog. Flags\n * and names come from flagcdn.com (CORS-friendly), fetched once and cached\n * for the session. Defaults to the visitor's region from navigator.language.\n */\n\ntype Country = { code: string; name: string; flag: string };\n\nconst flagUrl = (code: string) => `https://flagcdn.com/${code.toLowerCase()}.svg`;\n\nlet cache: Promise | null = null;\n\nfunction fetchCountries(): Promise {\n if (!cache) {\n cache = fetch(\"https://flagcdn.com/en/codes.json\")\n .then((r) => {\n if (!r.ok) throw new Error(\"Failed to load countries\");\n return r.json() as Promise>;\n })\n .then((data) =>\n Object.entries(data)\n .filter(([code]) => code.length === 2)\n .map(([code, name]) => ({ code: code.toUpperCase(), name, flag: flagUrl(code) }))\n .sort((a, b) => a.name.localeCompare(b.name)),\n )\n .catch((e: unknown) => {\n cache = null;\n throw e;\n });\n }\n return cache;\n}\n\n/** Loads the country list. Shared across every CountrySelect on the page. */\nfunction useCountries() {\n const [state, setState] = React.useState<{ data: Country[]; loading: boolean; error: string | null }>({\n data: [],\n loading: true,\n error: null,\n });\n React.useEffect(() => {\n let live = true;\n fetchCountries().then(\n (data) => live && setState({ data, loading: false, error: null }),\n (e: unknown) => live && setState({ data: [], loading: false, error: e instanceof Error ? e.message : \"Failed to load countries\" }),\n );\n return () => {\n live = false;\n };\n }, []);\n return state;\n}\n\nfunction userRegion(): string | undefined {\n if (typeof navigator === \"undefined\") return undefined;\n const locale = navigator.language || navigator.languages?.[0] || \"\";\n return locale.split(/[-_]/)[1]?.toUpperCase();\n}\n\nfunction Flag({ src, className }: { src: string; className?: string }) {\n return (\n \n \"\"\n \n );\n}\n\ntype CountrySelectProps = {\n /** ISO 3166-1 alpha-2 code, e.g. \"US\". */\n value?: string;\n defaultValue?: string;\n onValueChange?: (code: string, country: Country) => void;\n /** Use the visitor's locale region when uncontrolled and no default is given. */\n detectRegion?: boolean;\n title?: string;\n placeholder?: string;\n /** Show the selected country's name next to the flag. */\n showName?: boolean;\n className?: string;\n disabled?: boolean;\n};\n\nfunction CountrySelect({\n value: valueProp,\n defaultValue,\n onValueChange,\n detectRegion = true,\n title = \"Select your region\",\n placeholder = \"Search by country or region\",\n showName = false,\n className,\n disabled,\n}: CountrySelectProps) {\n const { data, loading, error } = useCountries();\n const [open, setOpen] = React.useState(false);\n const [query, setQuery] = React.useState(\"\");\n const [inner, setInner] = React.useState(defaultValue ?? (detectRegion ? userRegion() : undefined));\n const value = valueProp ?? inner ?? \"US\";\n const selected = data.find((c) => c.code === value);\n\n const filtered = React.useMemo(() => {\n const q = query.trim().toLowerCase();\n if (!q) return data;\n return data.filter((c) => c.name.toLowerCase().includes(q) || c.code.toLowerCase() === q);\n }, [data, query]);\n\n const pick = (c: Country) => {\n setInner(c.code);\n onValueChange?.(c.code, c);\n setOpen(false);\n };\n\n return (\n <>\n setOpen(true)}\n aria-haspopup=\"dialog\"\n aria-label={`Region: ${selected?.name ?? value}`}\n className={cn(\n \"state-layer focus-ring inline-flex h-9 items-center gap-1 rounded-full bg-container-high p-1.5 pr-2 text-sm font-medium transition-colors hover:bg-container-higher disabled:pointer-events-none disabled:opacity-50\",\n className,\n )}\n >\n \n {showName && {selected?.name ?? value}}\n \n \n\n {\n setOpen(o);\n if (!o) setQuery(\"\");\n }}\n >\n \n \n {title}\n Search and choose a country or region.\n \n \n\n
\n {loading ? (\n
    \n {Array.from({ length: 8 }, (_, i) => (\n
  • \n \n \n
  • \n ))}\n
\n ) : error ? (\n

{error}

\n ) : filtered.length === 0 ? (\n

No countries found

\n ) : (\n
    \n {filtered.map((c) => {\n const active = c.code === value;\n return (\n
  • \n pick(c)}\n className={cn(\n \"state-layer focus-ring flex h-14 w-full items-center gap-3 rounded-xl px-3 text-left text-sm font-medium tracking-tight transition-colors\",\n active && \"bg-container\",\n )}\n >\n \n {c.name}\n {active && }\n \n
  • \n );\n })}\n
\n )}\n
\n
\n \n \n );\n}\n\nexport { CountrySelect, useCountries };\nexport type { Country, CountrySelectProps };\n" } ], "categories": [ "motion" ], "meta": { "group": "dx", "docs": "https://dxuireact.com/docs/components/country-select" } }