Debounced coordinate resolver backed by the Mapbase /v1/geocode/resolve endpoint. Watches lng/lat props and surfaces administrative hierarchy.
Enter valid coordinates to resolve administrative context.
bunx shadcn@latest add https://mapbase.dev/r/geocode-resolve-input.jsonThe shadcn CLI copies the file(s) below into your tree, installs npm dependencies, and recursively pulls registry dependencies (including from cross-registry URLs).
None.
"use client"
import * as React from "react"
import {
Mapbase,
MapbaseError,
type CountryCode,
type GeocodeResolveResponse,
} from "mapbase"
import { cn } from "@/lib/utils"
export type GeocodeResolveInputProps = {
/**
* Public Mapbase API key (`mb_live_*`). Mint one at
* https://dashboard.mapbase.dev/api-keys.
*/
apiKey: string
/** Optional: pin the engine URL. Defaults to https://api.mapbase.dev. */
baseUrl?: string
/** Longitude (WGS84). */
lng: number
/** Latitude (WGS84). */
lat: number
/** Optional ISO country filter passed to the engine. */
country?: CountryCode
/** Fired when a resolve completes (or clears on invalid coords). */
onChange?: (result: GeocodeResolveResponse | null) => void
/** Debounce window for coordinate changes. Defaults to 300 ms. */
debounceMs?: number
/** Disable outbound requests. */
disabled?: boolean
className?: string
}
/**
* Headless coordinate resolver backed by the Mapbase
* `/v1/geocode/resolve` endpoint. Watches `lng` / `lat` (and optional
* `country`), debounces, aborts stale requests, and surfaces the
* administrative hierarchy inline.
*/
export function GeocodeResolveInput({
apiKey,
baseUrl,
lng,
lat,
country,
onChange,
debounceMs = 300,
disabled,
className,
}: GeocodeResolveInputProps) {
const [result, setResult] = React.useState<GeocodeResolveResponse | null>(
null,
)
const [loading, setLoading] = React.useState(false)
const [error, setError] = React.useState<MapbaseError | null>(null)
const onChangeRef = React.useRef(onChange)
onChangeRef.current = onChange
const client = React.useMemo(
() => new Mapbase(apiKey, baseUrl ? { baseUrl } : undefined),
[apiKey, baseUrl],
)
const countryKey = country ?? ""
React.useEffect(() => {
if (disabled) return
if (!Number.isFinite(lng) || !Number.isFinite(lat)) {
setResult(null)
setError(null)
setLoading(false)
onChangeRef.current?.(null)
return
}
const controller = new AbortController()
const timer = window.setTimeout(async () => {
setLoading(true)
setError(null)
const response = await client.geocode.resolve(
{
lng,
lat,
...(country ? { country } : {}),
},
{ signal: controller.signal },
)
if (controller.signal.aborted) return
if (response.error) {
if (
response.error.code === "network_error" &&
controller.signal.aborted
) {
return
}
setError(response.error)
setResult(null)
onChangeRef.current?.(null)
} else {
setResult(response.data)
onChangeRef.current?.(response.data)
}
setLoading(false)
}, debounceMs)
return () => {
controller.abort()
window.clearTimeout(timer)
}
}, [client, lng, lat, countryKey, debounceMs, disabled])
return (
<div className={cn("flex flex-col gap-2 text-sm", className)}>
{loading ? (
<p className="text-xs text-muted-foreground">Resolving…</p>
) : null}
{error ? (
<div className="rounded-md border border-destructive/40 bg-destructive/5 p-3 text-sm">
<p className="font-medium text-destructive">
{humanizeErrorCode(error.code)}
</p>
<p className="text-muted-foreground">{error.message}</p>
</div>
) : null}
{!loading && !error && result ? (
<ResolveSummary result={result} />
) : null}
{!loading && !error && !result ? (
<p className="text-xs text-muted-foreground">
Enter valid coordinates to resolve administrative context.
</p>
) : null}
</div>
)
}
function ResolveSummary({ result }: { result: GeocodeResolveResponse }) {
const slots = [
{
label: "ad_1",
name: result.ad_1,
id: result.ad_1_id,
uri: result.ad_1_uri,
kind: result.ad_1_kind,
},
{
label: "ad_2",
name: result.ad_2,
id: result.ad_2_id,
uri: result.ad_2_uri,
kind: result.ad_2_kind,
},
{
label: "ad_3",
name: result.ad_3,
id: result.ad_3_id,
uri: result.ad_3_uri,
kind: result.ad_3_kind,
},
{
label: "ad_4",
name: result.ad_4,
id: result.ad_4_id,
uri: result.ad_4_uri,
kind: result.ad_4_kind,
},
].filter((row) => row.name)
return (
<div className="rounded-md border bg-muted/40 p-3">
<div className="font-medium">
{result.anchor.name}{" "}
<span className="text-xs font-normal text-muted-foreground">
({result.country})
</span>
</div>
{slots.length > 0 ? (
<ul className="mt-2 space-y-1 text-xs text-muted-foreground">
{slots.map((row) => (
<li key={row.label}>
<span className="font-mono">{row.label}</span>: {row.name}
{row.id ? ` · ${row.id}` : ""}
{row.uri ? ` · ${row.uri}` : ""}
{row.kind ? ` · ${row.kind}` : ""}
</li>
))}
</ul>
) : null}
{result.zones.length > 0 ? (
<p className="mt-2 text-xs text-muted-foreground">
Zones: {result.zones.join(", ")}
</p>
) : null}
</div>
)
}
function humanizeErrorCode(code: MapbaseError["code"]): string {
switch (code) {
case "unauthorized":
return "Invalid API key"
case "rate_limited":
return "Rate limited — try again shortly"
case "invalid_request":
return "Invalid coordinates"
case "not_found":
return "No location at this point"
case "not_implemented":
return "Endpoint not yet available"
case "network_error":
return "Network error"
default:
return "Something went wrong"
}
}