{
  "name": "location-picker",
  "type": "registry:component",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "files": [
    {
      "type": "registry:component",
      "content": "\"use client\";\n\n/**\n * @author: @dorianbaffier\n * @description: Location Picker\n * @version: 1.0.0\n * @date: 2026-10-22\n * @license: MIT\n * @website: https://kokonutui.com\n * @github: https://github.com/kokonut-labs/kokonutui\n */\n\nimport { Check, Footprints, LocateFixed } from \"lucide-react\";\nimport {\n  AnimatePresence,\n  animate,\n  motion,\n  useMotionValue,\n  useReducedMotion,\n  useTransform,\n} from \"motion/react\";\nimport {\n  type KeyboardEvent,\n  type PointerEvent,\n  useEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\nconst GLIDE = { type: \"spring\", stiffness: 260, damping: 32 } as const;\n/** The drawn town, in px. */\nconst W = 1200;\nconst H = 1080;\n/** Map pixels walked in a minute. */\nconst WALK = 70;\n/** A pickup this close to a crossing is named by the crossing. */\nconst CORNER = 30;\nconst KEY_STEP = 64;\n/** The town is drawn at this scale, so a few blocks fit around the pin. */\nconst ZOOM = 0.62;\n\ninterface Street {\n  name: string;\n  width: number;\n  from: [number, number];\n  to: [number, number];\n}\n\nconst AVENUES = [\n  \"Mercer Ave\",\n  \"Hudson Ave\",\n  \"Grand Ave\",\n  \"Orchard Ave\",\n  \"Bowery\",\n];\nconst AVENUE_X = [180, 380, 580, 780, 980];\nconst CROSS_Y = [120, 280, 440, 600, 760, 920];\n\nconst STREETS: Street[] = [\n  ...AVENUES.map(\n    (name, i): Street => ({\n      name,\n      width: 12,\n      from: [AVENUE_X[i], 0],\n      to: [AVENUE_X[i], H],\n    })\n  ),\n  ...CROSS_Y.map(\n    (y, i): Street => ({\n      name: `${[\"1st\", \"2nd\", \"3rd\", \"4th\", \"5th\", \"6th\"][i]} St`,\n      width: 8,\n      from: [0, y],\n      to: [W, y],\n    })\n  ),\n  { name: \"Broadway\", width: 12, from: [120, 1020], to: [1140, 60] },\n];\n\n/** No pickups in the river or the park. */\nconst RIVER_X = 112;\nconst PARK = { x: 598, y: 456, w: 166, h: 128 };\nconst YOU: [number, number] = [470, 660];\n\nconst inPark = (x: number, y: number) =>\n  x > PARK.x - 4 &&\n  x < PARK.x + PARK.w + 4 &&\n  y > PARK.y - 4 &&\n  y < PARK.y + PARK.h + 4;\n\n/** Points along every street where a car can stop. */\nconst STOPS = STREETS.flatMap((street, s) => {\n  const [x0, y0] = street.from;\n  const [x1, y1] = street.to;\n  const steps = Math.ceil(Math.hypot(x1 - x0, y1 - y0) / 6);\n  const out: { x: number; y: number; s: number; t: number }[] = [];\n  for (let k = 0; k <= steps; k++) {\n    const t = k / steps;\n    const x = x0 + (x1 - x0) * t;\n    const y = y0 + (y1 - y0) * t;\n    if (x > RIVER_X + 14 && !inPark(x, y) && y > 20 && y < H - 20) {\n      out.push({ x, y, s, t });\n    }\n  }\n  return out;\n});\n\nfunction nearestStop(x: number, y: number) {\n  let best = STOPS[0];\n  let bestD = Number.POSITIVE_INFINITY;\n  for (const stop of STOPS) {\n    const d = (stop.x - x) ** 2 + (stop.y - y) ** 2;\n    if (d < bestD) {\n      bestD = d;\n      best = stop;\n    }\n  }\n  return best;\n}\n\nconst distanceTo = (street: Street, x: number, y: number) => {\n  const [x0, y0] = street.from;\n  const [x1, y1] = street.to;\n  const dx = x1 - x0;\n  const dy = y1 - y0;\n  const t = Math.max(\n    0,\n    Math.min(1, ((x - x0) * dx + (y - y0) * dy) / (dx * dx + dy * dy))\n  );\n  return Math.hypot(x0 + dx * t - x, y0 + dy * t - y);\n};\n\n/** \"Grand Ave & 3rd St\" at a corner, \"214 Grand Ave\" along the block. */\nfunction addressFor(stop: (typeof STOPS)[number]) {\n  const street = STREETS[stop.s];\n  const corner = STREETS.find(\n    (other, i) => i !== stop.s && distanceTo(other, stop.x, stop.y) < CORNER\n  );\n  if (corner) {\n    return `${street.name} & ${corner.name}`;\n  }\n  const number = 100 + Math.round(stop.t * 220) * 2;\n  return `${number} ${street.name}`;\n}\n\ninterface Lot {\n  x: number;\n  y: number;\n  w: number;\n  h: number;\n}\n\n/** Split a block into lots and leave a few empty, like a real one. */\nfunction lotsIn(block: Lot, rand: () => number, out: Lot[]) {\n  const cols = block.w > 120 ? 3 : 2;\n  const rows = block.h > 100 ? 2 : 1;\n  const gap = 8;\n  const lw = (block.w - gap * (cols - 1)) / cols;\n  const lh = (block.h - gap * (rows - 1)) / rows;\n  for (let a = 0; a < cols; a++) {\n    for (let b = 0; b < rows; b++) {\n      const shrink = rand() * 0.25;\n      if (rand() > 0.25) {\n        out.push({\n          x: block.x + a * (lw + gap),\n          y: block.y + b * (lh + gap),\n          w: lw * (1 - shrink),\n          h: lh * (1 - shrink * 0.6),\n        });\n      }\n    }\n  }\n}\n\n/** A seeded town of buildings, so every block has a little texture. */\nfunction buildings() {\n  let seed = 7;\n  const rand = () => {\n    seed = (seed * 9301 + 49_297) % 233_280;\n    return seed / 233_280;\n  };\n  const xs = [RIVER_X + 14, ...AVENUE_X, W];\n  const ys = [0, ...CROSS_Y, H];\n  const out: Lot[] = [];\n  for (let i = 0; i < xs.length - 1; i++) {\n    for (let j = 0; j < ys.length - 1; j++) {\n      const block = {\n        x: xs[i] + 14,\n        y: ys[j] + 12,\n        w: xs[i + 1] - xs[i] - 28,\n        h: ys[j + 1] - ys[j] - 24,\n      };\n      const open = inPark(block.x + block.w / 2, block.y + block.h / 2);\n      if (block.w >= 20 && block.h >= 20 && !open) {\n        lotsIn(block, rand, out);\n      }\n    }\n  }\n  return out;\n}\n\nconst BUILDINGS = buildings();\n/** Where the pickup starts: the curb a block or so from you. */\nconst START = nearestStop(YOU[0] + 110, YOU[1] + 40);\n\nfunction Town() {\n  return (\n    <>\n      <rect className=\"fill-zinc-100 dark:fill-zinc-900\" height={H} width={W} />\n      {BUILDINGS.map((b) => (\n        <rect\n          className=\"fill-zinc-200 dark:fill-zinc-800\"\n          height={b.h}\n          key={`${b.x.toFixed(1)}-${b.y.toFixed(1)}`}\n          rx={3}\n          width={b.w}\n          x={b.x}\n          y={b.y}\n        />\n      ))}\n      {/* Roads: a casing, then the tarmac on top. */}\n      {STREETS.map((s) => (\n        <line\n          className=\"stroke-zinc-200 dark:stroke-zinc-950\"\n          key={`casing-${s.name}`}\n          strokeLinecap=\"round\"\n          strokeWidth={s.width + 3}\n          x1={s.from[0]}\n          x2={s.to[0]}\n          y1={s.from[1]}\n          y2={s.to[1]}\n        />\n      ))}\n      {STREETS.map((s) => (\n        <line\n          className=\"stroke-white dark:stroke-zinc-700\"\n          key={`road-${s.name}`}\n          strokeLinecap=\"round\"\n          strokeWidth={s.width}\n          x1={s.from[0]}\n          x2={s.to[0]}\n          y1={s.from[1]}\n          y2={s.to[1]}\n        />\n      ))}\n      <rect\n        className=\"fill-emerald-100 stroke-emerald-200 dark:fill-emerald-950 dark:stroke-emerald-900\"\n        height={PARK.h}\n        rx={12}\n        width={PARK.w}\n        x={PARK.x}\n        y={PARK.y}\n      />\n      <ellipse\n        className=\"fill-sky-200 dark:fill-sky-900\"\n        cx={PARK.x + 112}\n        cy={PARK.y + 82}\n        rx={30}\n        ry={18}\n      />\n      <text\n        className=\"fill-emerald-700 font-medium text-[15px] dark:fill-emerald-400\"\n        x={PARK.x + 14}\n        y={PARK.y + 26}\n      >\n        Linden Park\n      </text>\n      <path\n        className=\"fill-sky-100 dark:fill-sky-950\"\n        d={`M0 0 H${RIVER_X - 6} C${RIVER_X + 10} 200 ${RIVER_X - 20} 380 ${RIVER_X} 560 S ${RIVER_X - 10} 900 ${RIVER_X + 4} ${H} H0 Z`}\n      />\n      <text\n        className=\"fill-sky-600 text-[15px] italic dark:fill-sky-400\"\n        transform={\"translate(60 640) rotate(-90)\"}\n      >\n        Mill River\n      </text>\n      {AVENUES.map((name, i) =>\n        [200, 680].map((y) => (\n          <text\n            className=\"fill-zinc-400 font-medium text-[14px] dark:fill-zinc-500\"\n            key={`${name}-${y}`}\n            transform={`translate(${AVENUE_X[i] + 12} ${y}) rotate(90)`}\n          >\n            {name}\n          </text>\n        ))\n      )}\n      {CROSS_Y.map((y, i) => (\n        <text\n          className=\"fill-zinc-400 font-medium text-[14px] dark:fill-zinc-500\"\n          key={y}\n          x={AVENUE_X[i % 2 === 0 ? 0 : 3] + 30}\n          y={y - 9}\n        >\n          {STREETS[AVENUES.length + i].name}\n        </text>\n      ))}\n    </>\n  );\n}\n\nfunction Pin({ lifted, spring }: { lifted: boolean; spring: object }) {\n  return (\n    <div className=\"pointer-events-none absolute top-1/2 left-1/2\">\n      {/* The shadow stays on the street while the pin lifts away from it. */}\n      <motion.span\n        animate={{\n          opacity: lifted ? 0.18 : 0.38,\n          scaleX: lifted ? 0.6 : 1,\n        }}\n        className=\"absolute -top-[3px] -left-[8px] block h-[6px] w-4 rounded-[50%] bg-black blur-[1px]\"\n        initial={false}\n        transition={spring}\n      />\n      <motion.div\n        animate={{ y: lifted ? -14 : 0 }}\n        className=\"absolute bottom-0 left-0 -translate-x-1/2\"\n        initial={false}\n        transition={spring}\n      >\n        <svg\n          aria-hidden=\"true\"\n          className=\"block drop-shadow-[0_2px_3px_rgb(0_0_0/0.25)]\"\n          height=\"38\"\n          viewBox=\"0 0 28 38\"\n          width=\"28\"\n        >\n          <path\n            className=\"fill-zinc-900 dark:fill-zinc-50\"\n            d=\"M14 37 L8.5 23.5 A12.5 12.5 0 1 1 19.5 23.5 Z\"\n          />\n          <circle\n            className=\"fill-white dark:fill-zinc-900\"\n            cx=\"14\"\n            cy=\"13\"\n            r=\"4.5\"\n          />\n        </svg>\n      </motion.div>\n    </div>\n  );\n}\n\ntype Status = \"moving\" | \"locating\" | \"ready\";\n\nfunction AddressLine({\n  status,\n  address,\n  minutes,\n  reduceMotion,\n}: {\n  status: Status;\n  address: string;\n  minutes: number;\n  reduceMotion: boolean;\n}) {\n  let line = address;\n  if (status === \"moving\") {\n    line = \"Move the map to set your pickup\";\n  } else if (status === \"locating\") {\n    line = \"Finding the curb…\";\n  }\n  return (\n    <div\n      aria-live=\"polite\"\n      className=\"relative h-10 min-w-0 flex-1 overflow-hidden\"\n    >\n      <AnimatePresence initial={false} mode=\"popLayout\">\n        <motion.div\n          animate={{ opacity: 1, y: 0 }}\n          className=\"absolute inset-0 flex flex-col justify-center leading-tight\"\n          exit={{ opacity: 0, y: -8 }}\n          initial={{ opacity: 0, y: 8 }}\n          key={line}\n          transition={{ duration: reduceMotion ? 0 : 0.18, ease: EASE_OUT }}\n        >\n          <p\n            className={cn(\n              \"truncate text-sm\",\n              status === \"ready\"\n                ? \"font-medium text-zinc-900 dark:text-zinc-100\"\n                : \"text-zinc-500 dark:text-zinc-400\"\n            )}\n          >\n            {line}\n          </p>\n          {status === \"ready\" && (\n            <p className=\"flex items-center gap-1 text-xs text-zinc-500 tabular-nums dark:text-zinc-400\">\n              <Footprints aria-hidden className=\"size-3\" />\n              {minutes < 1 ? \"Right where you are\" : `${minutes} min walk`}\n            </p>\n          )}\n        </motion.div>\n      </AnimatePresence>\n    </div>\n  );\n}\n\nexport interface LocationPickerProps {\n  /** Called with the street address when the pickup is confirmed. */\n  onConfirm?: (pickup: { address: string; x: number; y: number }) => void;\n  className?: string;\n}\n\n/**\n * Setting a pickup point the way ride apps do. Drag the map under a fixed\n * pin: the pin lifts off its shadow while you move, the map glides so it\n * lands on the nearest curb, and the pin drops and the street rolls in.\n * The river and the park won't take a pickup.\n */\nexport default function LocationPicker({\n  onConfirm,\n  className,\n}: LocationPickerProps) {\n  const mapRef = useRef<HTMLButtonElement>(null);\n  const cx = useMotionValue(START.x);\n  const cy = useMotionValue(START.y);\n  const [view, setView] = useState({ w: 360, h: 300 });\n  const [lifted, setLifted] = useState(false);\n  const [status, setStatus] = useState<Status>(\"ready\");\n  const [stop, setStop] = useState(START);\n  const [confirmed, setConfirmed] = useState(false);\n  const drag = useRef({ x: 0, y: 0, cx: 0, cy: 0, moved: false, on: false });\n  const timer = useRef<ReturnType<typeof setTimeout>>(undefined);\n  const reduceMotion = Boolean(useReducedMotion());\n\n  const tx = useTransform(cx, (v) => -v * ZOOM);\n  const ty = useTransform(cy, (v) => -v * ZOOM);\n\n  useEffect(() => {\n    const el = mapRef.current;\n    if (!el) {\n      return;\n    }\n    const measure = () => setView({ w: el.clientWidth, h: el.clientHeight });\n    measure();\n    const observer = new ResizeObserver(measure);\n    observer.observe(el);\n    return () => {\n      observer.disconnect();\n      clearTimeout(timer.current);\n      // A glide finishing after unmount would start the lookup timer again.\n      cx.stop();\n      cy.stop();\n    };\n  }, [cx, cy]);\n\n  const clampX = (v: number) =>\n    Math.min(W - view.w / 2 / ZOOM, Math.max(view.w / 2 / ZOOM, v));\n  const clampY = (v: number) =>\n    Math.min(H - view.h / 2 / ZOOM, Math.max(view.h / 2 / ZOOM, v));\n\n  /** Glide to the curb nearest (x, y), then put the pin down there. */\n  const settle = (x: number, y: number) => {\n    const next = nearestStop(x, y);\n    setLifted(true);\n    setStatus(\"locating\");\n    setConfirmed(false);\n    clearTimeout(timer.current);\n    const land = () => {\n      setLifted(false);\n      // A beat to \"look up\" the street, like a real geocoder would.\n      timer.current = setTimeout(\n        () => {\n          setStop(next);\n          setStatus(\"ready\");\n        },\n        reduceMotion ? 0 : 260\n      );\n    };\n    if (reduceMotion) {\n      cx.set(next.x);\n      cy.set(next.y);\n      land();\n      return;\n    }\n    animate(cx, next.x, GLIDE);\n    animate(cy, next.y, { ...GLIDE, onComplete: land });\n  };\n\n  const onDown = (event: PointerEvent<HTMLButtonElement>) => {\n    if (event.button !== 0) {\n      return;\n    }\n    cx.stop();\n    cy.stop();\n    event.currentTarget.setPointerCapture(event.pointerId);\n    drag.current = {\n      x: event.clientX,\n      y: event.clientY,\n      cx: cx.get(),\n      cy: cy.get(),\n      moved: false,\n      on: true,\n    };\n  };\n\n  const onMove = (event: PointerEvent<HTMLButtonElement>) => {\n    const d = drag.current;\n    if (!d.on) {\n      return;\n    }\n    const dx = event.clientX - d.x;\n    const dy = event.clientY - d.y;\n    if (!d.moved && Math.hypot(dx, dy) > 4) {\n      d.moved = true;\n      clearTimeout(timer.current);\n      setLifted(true);\n      setStatus(\"moving\");\n      setConfirmed(false);\n    }\n    if (d.moved) {\n      cx.set(clampX(d.cx - dx / ZOOM));\n      cy.set(clampY(d.cy - dy / ZOOM));\n    }\n  };\n\n  const onUp = (event: PointerEvent<HTMLButtonElement>) => {\n    const d = drag.current;\n    if (!d.on) {\n      return;\n    }\n    d.on = false;\n    if (d.moved) {\n      settle(cx.get(), cy.get());\n      return;\n    }\n    // A tap moves the pickup to where you tapped.\n    const rect = event.currentTarget.getBoundingClientRect();\n    settle(\n      clampX(cx.get() + (event.clientX - rect.left - view.w / 2) / ZOOM),\n      clampY(cy.get() + (event.clientY - rect.top - view.h / 2) / ZOOM)\n    );\n  };\n\n  const onKey = (event: KeyboardEvent<HTMLButtonElement>) => {\n    const moves: Record<string, [number, number]> = {\n      ArrowUp: [0, -KEY_STEP],\n      ArrowDown: [0, KEY_STEP],\n      ArrowLeft: [-KEY_STEP, 0],\n      ArrowRight: [KEY_STEP, 0],\n    };\n    const move = moves[event.key];\n    if (move) {\n      event.preventDefault();\n      settle(clampX(cx.get() + move[0]), clampY(cy.get() + move[1]));\n    }\n  };\n\n  const address = addressFor(stop);\n  const minutes = Math.round(\n    Math.hypot(stop.x - YOU[0], stop.y - YOU[1]) / WALK\n  );\n  const spring = reduceMotion\n    ? { duration: 0 }\n    : { type: \"spring\" as const, duration: 0.42, bounce: 0.35 };\n\n  return (\n    <div\n      className={cn(\n        \"w-full max-w-sm rounded-3xl border border-zinc-200 bg-white p-2 dark:border-zinc-800 dark:bg-zinc-950\",\n        className\n      )}\n    >\n      <div className=\"relative\">\n        <button\n          aria-label={`Pickup map. ${status === \"ready\" ? `Pickup at ${address}.` : \"\"} Drag or use arrow keys to move it.`}\n          className=\"relative block h-72 w-full cursor-grab touch-none select-none overflow-hidden rounded-2xl outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 active:cursor-grabbing\"\n          onKeyDown={onKey}\n          onPointerCancel={onUp}\n          onPointerDown={onDown}\n          onPointerMove={onMove}\n          onPointerUp={onUp}\n          ref={mapRef}\n          type=\"button\"\n        >\n          <motion.svg\n            aria-hidden=\"true\"\n            className=\"absolute\"\n            height={H * ZOOM}\n            style={{ x: tx, y: ty, left: view.w / 2, top: view.h / 2 }}\n            viewBox={`0 0 ${W} ${H}`}\n            width={W * ZOOM}\n          >\n            <Town />\n            {/* The walk from you to the pickup. */}\n            <motion.line\n              animate={{ opacity: status === \"ready\" ? 1 : 0 }}\n              className=\"stroke-sky-500\"\n              initial={false}\n              strokeDasharray=\"0 7\"\n              strokeLinecap=\"round\"\n              strokeWidth={3}\n              transition={{ duration: reduceMotion ? 0 : 0.2 }}\n              x1={YOU[0]}\n              x2={stop.x}\n              y1={YOU[1]}\n              y2={stop.y}\n            />\n            <circle\n              className=\"fill-sky-500/15\"\n              cx={YOU[0]}\n              cy={YOU[1]}\n              r={22}\n            />\n            <circle\n              className=\"fill-sky-500 stroke-white dark:stroke-zinc-900\"\n              cx={YOU[0]}\n              cy={YOU[1]}\n              r={7}\n              strokeWidth={3}\n            />\n          </motion.svg>\n          <Pin lifted={lifted} spring={spring} />\n        </button>\n        <button\n          aria-label=\"Back to my location\"\n          className=\"absolute right-3 bottom-3 flex size-9 items-center justify-center rounded-full bg-white text-zinc-700 shadow-[0_1px_3px_rgb(0_0_0/0.15),0_0_0_1px_rgb(0_0_0/0.04)] outline-none transition-colors duration-150 hover:text-zinc-950 focus-visible:ring-2 focus-visible:ring-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:hover:text-white\"\n          onClick={() => settle(YOU[0], YOU[1])}\n          type=\"button\"\n        >\n          <LocateFixed aria-hidden className=\"size-4\" />\n        </button>\n      </div>\n\n      <div className=\"flex items-center gap-3 px-3 pt-3 pb-1\">\n        <AddressLine\n          address={address}\n          minutes={minutes}\n          reduceMotion={reduceMotion}\n          status={status}\n        />\n      </div>\n      <div className=\"p-1\">\n        <button\n          className={cn(\n            \"flex h-11 w-full items-center justify-center gap-2 rounded-xl font-medium text-sm outline-none transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-zinc-400 focus-visible:ring-offset-2 disabled:opacity-40 dark:focus-visible:ring-offset-zinc-950\",\n            confirmed\n              ? \"bg-emerald-600 text-white dark:bg-emerald-500 dark:text-emerald-950\"\n              : \"bg-zinc-900 text-white hover:bg-zinc-800 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-white\"\n          )}\n          disabled={status !== \"ready\"}\n          onClick={() => {\n            onConfirm?.({ address, x: stop.x, y: stop.y });\n            setConfirmed(true);\n          }}\n          type=\"button\"\n        >\n          <AnimatePresence initial={false} mode=\"popLayout\">\n            <motion.span\n              animate={{ opacity: 1, y: 0 }}\n              className=\"inline-flex items-center gap-2\"\n              exit={{ opacity: 0, y: -8 }}\n              initial={{ opacity: 0, y: 8 }}\n              key={confirmed ? \"done\" : \"confirm\"}\n              transition={{ duration: reduceMotion ? 0 : 0.18, ease: EASE_OUT }}\n            >\n              {confirmed ? (\n                <>\n                  <Check aria-hidden className=\"size-4\" strokeWidth={2.5} />\n                  Driver on the way\n                </>\n              ) : (\n                \"Confirm pickup\"\n              )}\n            </motion.span>\n          </AnimatePresence>\n        </button>\n      </div>\n    </div>\n  );\n}\n",
      "path": "/components/kokonutui/location-picker.tsx",
      "target": "components/kokonutui/location-picker.tsx"
    }
  ]
}