Map Location Picker
Original · freelocation picker with pin
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/map-location-picker.json"use client";
import {
useCallback,
useEffect,
useId,
useMemo,
useRef,
useState,
type KeyboardEvent as ReactKeyboardEvent,
type PointerEvent as ReactPointerEvent,
} from "react";
import { motion, useAnimationControls, useReducedMotion } from "motion/react";
type Pos = { x: number; y: number };
type Rect = { x: number; y: number; w: number; h: number; t: number };
type Place = { id: string; name: string; area: string; code: string; x: number; y: number };
const MAP_W = 1000;
const MAP_H = 625;
/* Geo frame: ~11.9 km east-west across the canvas, anchored on central Bengaluru. */
const LNG_MIN = 77.54;
const LNG_SPAN = 0.11;
const LAT_MAX = 12.9972;
const LAT_SPAN = 0.0672;
const M_PER_UNIT = 11.93;
const H_ROADS = [82, 196, 318, 442, 552];
const V_ROADS = [126, 268, 420, 574, 716, 862];
const MAJOR_H = 318;
const MAJOR_V = 420;
const H_LINES = [-20, ...H_ROADS, MAP_H + 20];
const V_LINES = [-20, ...V_ROADS, MAP_W + 20];
const PLACES: Place[] = [
{ id: "indiranagar", name: "100 Feet Road", area: "Indiranagar", code: "560038", x: 455, y: 244 },
{ id: "koramangala", name: "5th Block", area: "Koramangala", code: "560095", x: 620, y: 470 },
{ id: "hsr", name: "Sector 2", area: "HSR Layout", code: "560102", x: 826, y: 556 },
{ id: "church", name: "Church Street", area: "Ashok Nagar", code: "560001", x: 330, y: 318 },
{ id: "jayanagar", name: "4th Block", area: "Jayanagar", code: "560011", x: 306, y: 508 },
{ id: "whitefield", name: "ITPL Main Road", area: "Whitefield", code: "560066", x: 904, y: 150 },
{ id: "hebbal", name: "Hebbal Flyover", area: "Kempapura", code: "560024", x: 520, y: 70 },
{ id: "malleshwaram", name: "8th Cross", area: "Malleshwaram", code: "560003", x: 150, y: 150 },
];
const PARK = { x: 579, y: 201, w: 132, h: 112 };
const LAKE = { cx: 170, cy: 470, rx: 100, ry: 52 };
function makeRng(seed: number): () => number {
let s = seed >>> 0;
return () => {
s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
return s / 4294967296;
};
}
function edgeGap(line: number, major: number): number {
return line === major ? 13 : 5;
}
function buildCity(): { blocks: Rect[]; buildings: Rect[] } {
const rng = makeRng(20260717);
const blocks: Rect[] = [];
const buildings: Rect[] = [];
for (let i = 0; i < V_LINES.length - 1; i++) {
for (let j = 0; j < H_LINES.length - 1; j++) {
const x0 = V_LINES[i] + edgeGap(V_LINES[i], MAJOR_V);
const x1 = V_LINES[i + 1] - edgeGap(V_LINES[i + 1], MAJOR_V);
const y0 = H_LINES[j] + edgeGap(H_LINES[j], MAJOR_H);
const y1 = H_LINES[j + 1] - edgeGap(H_LINES[j + 1], MAJOR_H);
blocks.push({ x: x0, y: y0, w: x1 - x0, h: y1 - y0, t: 0 });
for (let bx = x0 + 7; bx + 30 < x1 - 5; bx += 38) {
for (let by = y0 + 7; by + 26 < y1 - 5; by += 34) {
if (rng() < 0.44) continue;
buildings.push({
x: bx,
y: by,
w: 16 + rng() * 14,
h: 13 + rng() * 13,
t: Math.floor(rng() * 3),
});
}
}
}
}
return { blocks, buildings };
}
const CITY = buildCity();
const BUILDING_TONES = [
"fill-slate-300/80 dark:fill-zinc-800",
"fill-slate-300/50 dark:fill-zinc-800/70",
"fill-slate-400/45 dark:fill-zinc-700/50",
];
function clamp(v: number, lo: number, hi: number): number {
return v < lo ? lo : v > hi ? hi : v;
}
function snapToRoad(p: Pos): Pos {
let bestH = H_ROADS[0];
let dh = Number.POSITIVE_INFINITY;
for (const h of H_ROADS) {
const d = Math.abs(p.y - h);
if (d < dh) {
dh = d;
bestH = h;
}
}
let bestV = V_ROADS[0];
let dv = Number.POSITIVE_INFINITY;
for (const v of V_ROADS) {
const d = Math.abs(p.x - v);
if (d < dv) {
dv = d;
bestV = v;
}
}
return dh <= dv ? { x: p.x, y: bestH } : { x: bestV, y: p.y };
}
function nearest(p: Pos): { place: Place; dist: number } {
let best = PLACES[0];
let bestD = Number.POSITIVE_INFINITY;
for (const pl of PLACES) {
const d = Math.hypot(pl.x - p.x, pl.y - p.y);
if (d < bestD) {
bestD = d;
best = pl;
}
}
return { place: best, dist: bestD };
}
const COMPASS = ["E", "NE", "N", "NW", "W", "SW", "S", "SE"];
function bearing(dx: number, dy: number): string {
const deg = (Math.atan2(-dy, dx) * 180) / Math.PI;
return COMPASS[Math.round(((deg + 360) % 360) / 45) % 8];
}
export default function MapLocationPicker() {
const reduce = useReducedMotion();
const controls = useAnimationControls();
const uid = useId();
const listId = `${uid}-suggestions`;
const inputId = `${uid}-search`;
const hintId = `${uid}-hint`;
const [pin, setPin] = useState<Pos>({ x: 455, y: 244 });
const [radiusM, setRadiusM] = useState(600);
const [snap, setSnap] = useState(false);
const [query, setQuery] = useState("");
const [open, setOpen] = useState(false);
const [active, setActive] = useState(0);
const [dropSeq, setDropSeq] = useState(0);
const [confirmed, setConfirmed] = useState(false);
const [copied, setCopied] = useState(false);
const [announce, setAnnounce] = useState("");
const surfaceRef = useRef<HTMLDivElement | null>(null);
const pinRef = useRef<HTMLButtonElement | null>(null);
const draggingRef = useRef(false);
const grabRef = useRef<Pos>({ x: 0, y: 0 });
const copyTimer = useRef<number | null>(null);
const suggestions = useMemo(() => {
const q = query.trim().toLowerCase();
if (q === "") return PLACES;
return PLACES.filter((p) =>
`${p.name} ${p.area} ${p.code}`.toLowerCase().includes(q),
);
}, [query]);
const near = useMemo(() => nearest(pin), [pin]);
const lat = LAT_MAX - (pin.y / MAP_H) * LAT_SPAN;
const lng = LNG_MIN + (pin.x / MAP_W) * LNG_SPAN;
const latText = lat.toFixed(5);
const lngText = lng.toFixed(5);
const offsetM = Math.round(near.dist * M_PER_UNIT);
const dir = bearing(pin.x - near.place.x, pin.y - near.place.y);
const addressLine =
offsetM < 45
? `${near.place.name}, ${near.place.area}`
: `${offsetM} m ${dir} of ${near.place.name}, ${near.place.area}`;
const radiusUnits = radiusM / M_PER_UNIT;
const movePin = useCallback(
(next: Pos, snapping: boolean) => {
const raw = {
x: clamp(next.x, 0, MAP_W),
y: clamp(next.y, 0, MAP_H),
};
setPin(snapping ? snapToRoad(raw) : raw);
setConfirmed(false);
},
[],
);
useEffect(() => {
const t = window.setTimeout(() => {
setAnnounce(`${addressLine}. Latitude ${latText}, longitude ${lngText}.`);
}, 700);
return () => window.clearTimeout(t);
}, [addressLine, latText, lngText]);
useEffect(() => {
if (dropSeq === 0 || reduce) return;
void controls.start(
{ y: [-22, 0], scale: [0.85, 1.06, 1] },
{ duration: 0.45, ease: [0.16, 1, 0.3, 1] },
);
}, [dropSeq, reduce, controls]);
useEffect(() => {
return () => {
if (copyTimer.current !== null) window.clearTimeout(copyTimer.current);
};
}, []);
function toLocal(clientX: number, clientY: number): Pos | null {
const el = surfaceRef.current;
if (!el) return null;
const r = el.getBoundingClientRect();
if (r.width === 0 || r.height === 0) return null;
return {
x: ((clientX - r.left) / r.width) * MAP_W,
y: ((clientY - r.top) / r.height) * MAP_H,
};
}
function beginDrag(e: ReactPointerEvent<HTMLElement>, fromPin: boolean) {
if (e.button !== 0 && e.pointerType === "mouse") return;
const p = toLocal(e.clientX, e.clientY);
if (!p) return;
e.preventDefault();
const el = surfaceRef.current;
if (el) el.setPointerCapture(e.pointerId);
draggingRef.current = true;
grabRef.current = fromPin ? { x: pin.x - p.x, y: pin.y - p.y } : { x: 0, y: 0 };
if (!fromPin) movePin(p, snap);
pinRef.current?.focus();
}
function onSurfaceMove(e: ReactPointerEvent<HTMLDivElement>) {
if (!draggingRef.current) return;
const p = toLocal(e.clientX, e.clientY);
if (!p) return;
movePin({ x: p.x + grabRef.current.x, y: p.y + grabRef.current.y }, snap);
}
function endDrag(e: ReactPointerEvent<HTMLDivElement>) {
if (!draggingRef.current) return;
draggingRef.current = false;
const el = surfaceRef.current;
if (el?.hasPointerCapture(e.pointerId)) el.releasePointerCapture(e.pointerId);
}
function onPinKeyDown(e: ReactKeyboardEvent<HTMLButtonElement>) {
const step = e.shiftKey ? 28 : 7;
let dx = 0;
let dy = 0;
if (e.key === "ArrowLeft") dx = -step;
else if (e.key === "ArrowRight") dx = step;
else if (e.key === "ArrowUp") dy = -step;
else if (e.key === "ArrowDown") dy = step;
else return;
e.preventDefault();
movePin({ x: pin.x + dx, y: pin.y + dy }, snap);
}
function choose(place: Place) {
movePin({ x: place.x, y: place.y }, snap);
setQuery(`${place.name}, ${place.area}`);
setOpen(false);
setActive(0);
setDropSeq((n) => n + 1);
}
function onSearchKeyDown(e: ReactKeyboardEvent<HTMLInputElement>) {
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
e.preventDefault();
if (!open) {
setOpen(true);
setActive(0);
return;
}
if (suggestions.length === 0) return;
const delta = e.key === "ArrowDown" ? 1 : -1;
setActive((i) => (i + delta + suggestions.length) % suggestions.length);
return;
}
if (e.key === "Enter" && open) {
const picked = suggestions[active];
if (!picked) return;
e.preventDefault();
choose(picked);
return;
}
if (e.key === "Escape") {
e.preventDefault();
if (open) setOpen(false);
else setQuery("");
}
}
function toggleSnap() {
const next = !snap;
setSnap(next);
if (next) {
setPin((p) => snapToRoad(p));
setConfirmed(false);
}
}
async function copyCoords() {
try {
await navigator.clipboard.writeText(`${latText}, ${lngText}`);
setCopied(true);
if (copyTimer.current !== null) window.clearTimeout(copyTimer.current);
copyTimer.current = window.setTimeout(() => setCopied(false), 1800);
} catch {
setCopied(false);
}
}
const scaleBarUnits = 500 / M_PER_UNIT;
return (
<section className="relative w-full bg-slate-50 px-4 py-20 text-slate-900 sm:px-6 dark:bg-zinc-950 dark:text-zinc-100">
<style>{`
@keyframes mlp-ping {
0% { transform: scale(0.4); opacity: 0.5; }
70% { transform: scale(1); opacity: 0; }
100% { transform: scale(1); opacity: 0; }
}
@keyframes mlp-dash { to { stroke-dashoffset: -48; } }
.mlp-ping { animation: mlp-ping 2.6s cubic-bezier(0, 0, 0.2, 1) infinite; }
.mlp-dash { animation: mlp-dash 3.2s linear infinite; }
@media (prefers-reduced-motion: reduce) {
.mlp-ping, .mlp-dash { animation: none !important; }
}
`}</style>
<div className="mx-auto w-full max-w-6xl">
<div className="max-w-2xl">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-indigo-600 dark:text-indigo-400">
Coverage · Bengaluru
</p>
<h2 className="mt-3 text-3xl font-semibold tracking-tight sm:text-4xl">
Drop a pin on your pickup point
</h2>
<p className="mt-3 text-base leading-relaxed text-slate-600 dark:text-zinc-400">
Riders route to the pin, not the postcode. Drag the marker, search a landmark, or nudge
it with the arrow keys until it sits on the right gate.
</p>
</div>
<div className="mt-10 grid gap-6 lg:grid-cols-[minmax(0,1.65fr)_minmax(0,1fr)]">
{/* ---------------- Map ---------------- */}
<div className="relative overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
<div
ref={surfaceRef}
onPointerDown={(e) => beginDrag(e, false)}
onPointerMove={onSurfaceMove}
onPointerUp={endDrag}
onPointerCancel={endDrag}
className="relative aspect-[16/10] w-full cursor-crosshair touch-none select-none"
>
<svg
viewBox={`0 0 ${MAP_W} ${MAP_H}`}
className="absolute inset-0 h-full w-full"
aria-hidden="true"
focusable="false"
>
<rect
x={0}
y={0}
width={MAP_W}
height={MAP_H}
className="fill-white dark:fill-zinc-700"
/>
{CITY.blocks.map((b, i) => (
<rect
key={`b${i}`}
x={b.x}
y={b.y}
width={b.w}
height={b.h}
rx={3}
className="fill-slate-100 dark:fill-zinc-900"
/>
))}
{CITY.buildings.map((b, i) => (
<rect
key={`h${i}`}
x={b.x}
y={b.y}
width={b.w}
height={b.h}
rx={1.5}
className={BUILDING_TONES[b.t]}
/>
))}
{/* Park */}
<rect
x={PARK.x}
y={PARK.y}
width={PARK.w}
height={PARK.h}
rx={10}
className="fill-emerald-200 stroke-emerald-300 dark:fill-emerald-950 dark:stroke-emerald-900"
strokeWidth={1.5}
/>
{[
{ x: 600, y: 226 },
{ x: 634, y: 292 },
{ x: 688, y: 232 },
{ x: 664, y: 296 },
{ x: 700, y: 276 },
].map((t, i) => (
<circle
key={`t${i}`}
cx={t.x}
cy={t.y}
r={7}
className="fill-emerald-400/70 dark:fill-emerald-800"
/>
))}
<text
x={PARK.x + PARK.w / 2}
y={PARK.y + 62}
textAnchor="middle"
className="fill-emerald-700 text-[11px] font-medium dark:fill-emerald-400"
>
Defence Colony Park
</text>
{/* Lake */}
<ellipse
cx={LAKE.cx}
cy={LAKE.cy}
rx={LAKE.rx}
ry={LAKE.ry}
className="fill-sky-200 stroke-sky-300 dark:fill-sky-950 dark:stroke-sky-900"
strokeWidth={1.5}
/>
<text
x={LAKE.cx}
y={LAKE.cy + 4}
textAnchor="middle"
className="fill-sky-700 text-[11px] font-medium dark:fill-sky-400"
>
Ulsoor Lake
</text>
{/* Major-road centre lines */}
<line
x1={0}
y1={MAJOR_H}
x2={MAP_W}
y2={MAJOR_H}
strokeWidth={1.5}
strokeDasharray="12 10"
className="mlp-dash stroke-amber-400/70 dark:stroke-amber-500/40"
/>
<line
x1={MAJOR_V}
y1={0}
x2={MAJOR_V}
y2={MAP_H}
strokeWidth={1.5}
strokeDasharray="12 10"
className="mlp-dash stroke-amber-400/70 dark:stroke-amber-500/40"
/>
<text
x={772}
y={MAJOR_H + 4}
className="fill-slate-500 text-[10px] font-medium tracking-wide dark:fill-zinc-400"
>
100 Feet Rd
</text>
<text
x={MAJOR_V + 4}
y={598}
transform={`rotate(-90 ${MAJOR_V + 4} 598)`}
className="fill-slate-500 text-[10px] font-medium tracking-wide dark:fill-zinc-400"
>
Old Airport Rd
</text>
{/* Coverage radius */}
<circle
cx={pin.x}
cy={pin.y}
r={radiusUnits}
className="fill-indigo-500/12 stroke-indigo-500/70 dark:fill-indigo-400/12 dark:stroke-indigo-400/70"
strokeWidth={1.75}
strokeDasharray="7 6"
/>
{/* Landmarks */}
{PLACES.map((p) => {
const isNear = p.id === near.place.id;
const flip = p.x > 800;
return (
<g key={p.id} className="pointer-events-none">
<circle
cx={p.x}
cy={p.y}
r={isNear ? 5 : 3.5}
className={
isNear
? "fill-indigo-600 dark:fill-indigo-400"
: "fill-slate-400 dark:fill-zinc-500"
}
/>
<text
x={flip ? p.x - 9 : p.x + 9}
y={p.y + 3.5}
textAnchor={flip ? "end" : "start"}
className={
isNear
? "fill-indigo-700 text-[10px] font-semibold dark:fill-indigo-300"
: "fill-slate-500 text-[10px] dark:fill-zinc-400"
}
>
{p.area}
</text>
</g>
);
})}
{/* Scale bar */}
<g className="pointer-events-none">
<line
x1={28}
y1={598}
x2={28 + scaleBarUnits}
y2={598}
strokeWidth={2}
className="stroke-slate-600 dark:stroke-zinc-300"
/>
<line x1={28} y1={592} x2={28} y2={604} strokeWidth={2} className="stroke-slate-600 dark:stroke-zinc-300" />
<line
x1={28 + scaleBarUnits}
y1={592}
x2={28 + scaleBarUnits}
y2={604}
strokeWidth={2}
className="stroke-slate-600 dark:stroke-zinc-300"
/>
<text x={34 + scaleBarUnits} y={602} className="fill-slate-600 text-[10px] dark:fill-zinc-300">
500 m
</text>
</g>
{/* Compass */}
<g className="pointer-events-none">
<circle cx={956} cy={44} r={17} className="fill-white/85 stroke-slate-300 dark:fill-zinc-900/85 dark:stroke-zinc-700" strokeWidth={1} />
<path d="M956 32 L961 46 L956 43 L951 46 Z" className="fill-rose-500 dark:fill-rose-400" />
<text x={956} y={58} textAnchor="middle" className="fill-slate-600 text-[9px] font-semibold dark:fill-zinc-300">
N
</text>
</g>
</svg>
{/* Pulse under the pin */}
<div
className="pointer-events-none absolute h-16 w-16 -translate-x-1/2 -translate-y-1/2"
style={{ left: `${(pin.x / MAP_W) * 100}%`, top: `${(pin.y / MAP_H) * 100}%` }}
>
<span className="mlp-ping absolute inset-0 rounded-full bg-rose-500/60 dark:bg-rose-400/50" />
</div>
{/* Pin */}
<div
className="absolute"
style={{
left: `${(pin.x / MAP_W) * 100}%`,
top: `${(pin.y / MAP_H) * 100}%`,
transform: "translate(-50%, -100%)",
}}
>
<motion.div animate={controls} style={{ originX: 0.5, originY: 1 }}>
<button
ref={pinRef}
type="button"
onPointerDown={(e) => {
e.stopPropagation();
beginDrag(e, true);
}}
onKeyDown={onPinKeyDown}
aria-describedby={hintId}
aria-label={`Location pin at ${addressLine}, latitude ${latText}, longitude ${lngText}`}
className="block h-11 w-11 cursor-grab rounded-xl focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-600 focus-visible:ring-offset-2 focus-visible:ring-offset-white active:cursor-grabbing dark:focus-visible:ring-indigo-400 dark:focus-visible:ring-offset-zinc-900"
>
<svg viewBox="0 0 44 44" className="h-11 w-11 drop-shadow-md" aria-hidden="true">
<path
d="M22 44C22 44 36 29.6 36 21A14 14 0 1 0 8 21C8 29.6 22 44 22 44Z"
className="fill-rose-600 stroke-white dark:fill-rose-500 dark:stroke-zinc-900"
strokeWidth={2}
/>
<circle cx={22} cy={21} r={5.2} className="fill-white dark:fill-zinc-900" />
</svg>
</button>
</motion.div>
</div>
<p id={hintId} className="sr-only">
Drag the pin, or press the arrow keys to move it in seven-unit steps. Hold Shift for
larger steps. Clicking anywhere on the map also moves the pin.
</p>
</div>
<div className="flex flex-wrap items-center justify-between gap-3 border-t border-slate-200 px-4 py-3 text-xs text-slate-500 dark:border-zinc-800 dark:text-zinc-400">
<span>Tap the map or drag the pin to reposition</span>
<span className="font-mono tabular-nums">
{latText}, {lngText}
</span>
</div>
</div>
{/* ---------------- Panel ---------------- */}
<div className="flex flex-col gap-5 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
<div className="relative">
<label
htmlFor={inputId}
className="block text-sm font-medium text-slate-700 dark:text-zinc-300"
>
Search a landmark or PIN code
</label>
<div className="relative mt-2">
<svg
viewBox="0 0 20 20"
className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400 dark:text-zinc-500"
fill="none"
stroke="currentColor"
strokeWidth={1.8}
aria-hidden="true"
>
<circle cx={8.5} cy={8.5} r={5.5} />
<path d="M12.7 12.7 17 17" strokeLinecap="round" />
</svg>
<input
id={inputId}
type="text"
role="combobox"
autoComplete="off"
aria-expanded={open}
aria-controls={listId}
aria-autocomplete="list"
aria-activedescendant={
open && suggestions[active] ? `${listId}-opt-${active}` : undefined
}
value={query}
placeholder="Koramangala, 560095…"
onChange={(e) => {
setQuery(e.target.value);
setOpen(true);
setActive(0);
}}
onFocus={() => setOpen(true)}
onBlur={() => setOpen(false)}
onKeyDown={onSearchKeyDown}
className="w-full rounded-lg border border-slate-300 bg-white py-2.5 pl-9 pr-3 text-sm text-slate-900 placeholder:text-slate-400 focus:border-indigo-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-600 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100 dark:placeholder:text-zinc-500 dark:focus:border-indigo-400 dark:focus-visible:ring-indigo-400"
/>
</div>
{open && (
<motion.ul
id={listId}
role="listbox"
aria-label="Location suggestions"
initial={reduce ? false : { opacity: 0, y: -4 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.16 }}
className="absolute z-20 mt-2 max-h-64 w-full overflow-auto rounded-lg border border-slate-200 bg-white p-1 shadow-lg dark:border-zinc-700 dark:bg-zinc-900"
>
{suggestions.length === 0 && (
<li className="px-3 py-2 text-sm text-slate-500 dark:text-zinc-400">
No match. Drop the pin manually instead.
</li>
)}
{suggestions.map((p, i) => (
<li
key={p.id}
id={`${listId}-opt-${i}`}
role="option"
aria-selected={i === active}
onPointerDown={(e) => {
e.preventDefault();
choose(p);
}}
onMouseEnter={() => setActive(i)}
className={`flex cursor-pointer items-center justify-between gap-3 rounded-md px-3 py-2 text-sm ${
i === active
? "bg-indigo-50 text-indigo-900 dark:bg-indigo-500/15 dark:text-indigo-200"
: "text-slate-700 dark:text-zinc-300"
}`}
>
<span className="min-w-0">
<span className="block truncate font-medium">{p.name}</span>
<span className="block truncate text-xs text-slate-500 dark:text-zinc-400">
{p.area} · Bengaluru {p.code}
</span>
</span>
<span className="shrink-0 font-mono text-[10px] text-slate-400 tabular-nums dark:text-zinc-500">
{(LAT_MAX - (p.y / MAP_H) * LAT_SPAN).toFixed(3)}
</span>
</li>
))}
</motion.ul>
)}
</div>
<div className="flex items-start justify-between gap-4 rounded-lg border border-slate-200 p-3 dark:border-zinc-800">
<span className="text-sm">
<span className="block font-medium text-slate-800 dark:text-zinc-200">
Snap to nearest road
</span>
<span className="mt-0.5 block text-xs text-slate-500 dark:text-zinc-400">
Keeps the pin off rooftops so drivers get a reachable kerb.
</span>
</span>
<button
type="button"
role="switch"
aria-checked={snap}
aria-label="Snap the pin to the nearest road"
onClick={toggleSnap}
className={`relative mt-0.5 h-6 w-11 shrink-0 rounded-full transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-600 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-indigo-400 dark:focus-visible:ring-offset-zinc-900 ${
snap ? "bg-indigo-600 dark:bg-indigo-500" : "bg-slate-300 dark:bg-zinc-700"
}`}
>
<span
className={`absolute top-0.5 h-5 w-5 rounded-full bg-white transition-transform dark:bg-zinc-100 ${
snap ? "translate-x-[22px]" : "translate-x-0.5"
}`}
/>
</button>
</div>
<div>
<div className="flex items-baseline justify-between">
<label
htmlFor={`${uid}-radius`}
className="text-sm font-medium text-slate-700 dark:text-zinc-300"
>
Pickup radius
</label>
<span className="font-mono text-sm tabular-nums text-slate-600 dark:text-zinc-400">
{radiusM >= 1000 ? `${(radiusM / 1000).toFixed(1)} km` : `${radiusM} m`}
</span>
</div>
<input
id={`${uid}-radius`}
type="range"
min={100}
max={3000}
step={50}
value={radiusM}
onChange={(e) => {
setRadiusM(Number(e.target.value));
setConfirmed(false);
}}
className="mt-3 h-2 w-full cursor-pointer appearance-none rounded-full bg-slate-200 accent-indigo-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-600 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:bg-zinc-800 dark:accent-indigo-400 dark:focus-visible:ring-indigo-400 dark:focus-visible:ring-offset-zinc-900"
/>
<div className="mt-1 flex justify-between text-[11px] text-slate-400 dark:text-zinc-500">
<span>100 m</span>
<span>3 km</span>
</div>
</div>
<div className="rounded-lg bg-slate-50 p-4 dark:bg-zinc-950">
<p className="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-zinc-400">
Selected point
</p>
<p className="mt-2 text-sm font-medium leading-snug text-slate-900 dark:text-zinc-100">
{addressLine}
</p>
<p className="mt-0.5 text-xs text-slate-500 dark:text-zinc-400">
Bengaluru {near.place.code} · {snap ? "snapped to road" : "free placement"}
</p>
<dl className="mt-3 grid grid-cols-2 gap-2 border-t border-slate-200 pt-3 text-xs dark:border-zinc-800">
<div>
<dt className="text-slate-500 dark:text-zinc-500">Latitude</dt>
<dd className="font-mono tabular-nums text-slate-800 dark:text-zinc-200">{latText}</dd>
</div>
<div>
<dt className="text-slate-500 dark:text-zinc-500">Longitude</dt>
<dd className="font-mono tabular-nums text-slate-800 dark:text-zinc-200">{lngText}</dd>
</div>
</dl>
</div>
<div className="flex gap-2">
<button
type="button"
onClick={() => setConfirmed(true)}
className="flex-1 rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-indigo-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-600 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:bg-indigo-500 dark:hover:bg-indigo-400 dark:focus-visible:ring-indigo-400 dark:focus-visible:ring-offset-zinc-900"
>
Confirm pickup point
</button>
<button
type="button"
onClick={() => void copyCoords()}
aria-label="Copy coordinates to clipboard"
className="inline-flex items-center gap-1.5 rounded-lg border border-slate-300 px-3 py-2.5 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-600 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-800 dark:focus-visible:ring-indigo-400 dark:focus-visible:ring-offset-zinc-900"
>
{copied ? (
<svg viewBox="0 0 20 20" className="h-4 w-4 text-emerald-600 dark:text-emerald-400" fill="none" stroke="currentColor" strokeWidth={2} aria-hidden="true">
<path d="m4 10.5 4 4 8-9" strokeLinecap="round" strokeLinejoin="round" />
</svg>
) : (
<svg viewBox="0 0 20 20" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth={1.7} aria-hidden="true">
<rect x={7} y={7} width={9} height={9} rx={2} />
<path d="M13 5.5A1.5 1.5 0 0 0 11.5 4H5.5A1.5 1.5 0 0 0 4 5.5v6A1.5 1.5 0 0 0 5.5 13" strokeLinecap="round" />
</svg>
)}
{copied ? "Copied" : "Copy"}
</button>
</div>
<p role="status" className="min-h-[1.25rem] text-xs text-emerald-700 dark:text-emerald-400">
{confirmed
? `Saved. Riders will be routed to ${addressLine} within a ${
radiusM >= 1000 ? `${(radiusM / 1000).toFixed(1)} km` : `${radiusM} m`
} pickup radius.`
: ""}
</p>
<p aria-live="polite" className="sr-only">
{announce}
</p>
</div>
</div>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Map Card
Originallocation card with a hand-drawn SVG map

Map Store Locator
Originalstore locator list plus map

Map Contact
Originalcontact section with map and details
Map Delivery Tracking
Originaldelivery tracking with route

Map Pins List
Originalmap with a list of pinned places

Map Region Select
Originalclickable region selector map

Map Mini
Originalmini map preview widget

Spotlight Hero
OriginalA centred hero with a soft radial spotlight, badge and dual call-to-action.

Split Hero
OriginalA two-column hero pairing a headline and CTAs with a product mock and social proof.

Gradient Spotlight Hero
OriginalA minimal centred hero with a soft gradient-mesh backdrop, announcement pill and dual call-to-action buttons.

App Preview Hero
OriginalA centred hero that pairs headline copy with a realistic product dashboard mock built entirely from markup, complete with browser chrome and a floating notification card.

Waitlist Capture Hero
OriginalA dark, focused hero with an inline email capture form and avatar social proof, ready for pre-launch waitlists.

