# Map

Declarative wrapper around MapLibre GL JS. Compose `MapMarker`, `MapPolyline`, `MapPolygon`, `MapCircle`, and `MapCluster` as children — geometry inherits the platform's blue primary by default. Free OpenFreeMap tiles light/dark; switch via the `style` prop. RTL Arabic labels render correctly out of the box.

[View examples](https://marsam.rahhal.app/components/map) · [Agent index](https://marsam.rahhal.app/llms.txt)

**Import:** `@rahhal/ui (Map, MapMarker, MapPolyline, MapPolygon, MapCircle, MapCluster)`

**Category:** `surfaces`

## Setup

Install once at the consuming app's entry point:

```ts
// main.tsx / app/layout.tsx
import "maplibre-gl/dist/maplibre-gl.css";
```

## Components

| component | purpose |
| --- | --- |
| Map | root container; manages MapLibre instance lifecycle and exposes context |
| MapMarker | pin annotation with optional label + popup |
| MapPolyline | ordered line (route, path) |
| MapPolygon | filled, stroked area (neighborhood, region) |
| MapCircle | geographic radius circle ("within N km") |
| MapCluster | clustered point source — circles for groups, singles for stragglers, click-to-expand |
| useMap | hook for custom children that need raw MapLibre access |

## Coordinate convention

All positions are `[longitude, latitude]` — the GeoJSON / MapLibre convention. **Longitude first.** This trips up engineers used to Apple's CLLocationCoordinate2D (`latitude, longitude`); the order is reversed on web.

## MapMarker props

| prop | type | purpose |
| --- | --- | --- |
| lngLat | [number, number] | position as `[longitude, latitude]` (required) |
| label | string | accessible label · rendered as both `aria-label` and `title` tooltip |
| color | string | marker fill; defaults to the theme's `--primary` |
| popup | ReactNode | popup content shown on click — only string/number render rich; richer trees fall back to the label |
| anchor | MapLibre anchor | default `"bottom"` so the pin tip sits on the coordinate |
| element | HTMLElement | custom DOM to use instead of the default pin (e.g. an emoji bubble) |
| glyph | string | render the marker as a circular sticker pin with this glyph (emoji) centered, instead of MapLibre's default teardrop. Combines with `color` for the border tint, so the existing `color={selected ? accent : …}` selection pattern keeps working |
| onClick | () => void | fires when the user taps/clicks the marker · sets `cursor: pointer` automatically |

## Clustering

Wrap your `<Map>` with a `<MapCluster>` child when you have many points (hundreds+) and want them aggregated as zoom decreases — the canonical [MapLibre clustering pattern](https://maplibre.org/maplibre-gl-js/docs/examples/create-and-style-clusters/). Cluster circles auto-expand on click, single points fire `onPointClick`.

| prop | type | purpose |
| --- | --- | --- |
| points | MapClusterPoint[] | array of `{ lngLat, id?, properties? }` features to cluster (required). A point's `id` is forwarded to the GeoJSON feature id only when it's a number — string ids (e.g. UUIDs) break MapLibre's clustering and render nothing, so keep string identity on `properties` and read it back in `onPointClick` |
| clusterMaxZoom | number | max zoom at which clustering is active. default 14 |
| clusterRadius | number | KD-tree pixel radius used to group points. default 50 |
| color | string | cluster circle fill; defaults to the theme's `--primary` |
| countTextColor | string | color of the count label drawn on each cluster. default `#ffffff` |
| pointColor | string | fill for unclustered (single) points; defaults to the theme's `--primary` |
| pointRadius | number | radius (px) of unclustered points. default 8 |
| pointStrokeColor | string | stroke color of unclustered points. default `#ffffff` |
| pointStrokeWidth | number | stroke width (px) of unclustered points. default 2 |
| onPointClick | (properties, lngLat) => void | fires when an unclustered point is clicked · cursor flips to pointer over clusters and singles |

Cluster count labels render in the basemap's font (`Noto Sans Regular`, available in the default OpenFreeMap styles). Like other geometry layers, `MapCluster` re-adds its source + layers automatically when the basemap style changes (theme switch).

## Controls

| prop | type | purpose |
| --- | --- | --- |
| showNavigation | boolean | zoom + compass control (top-end). default true |
| showLocateControl | boolean | "locate me" control wrapping MapLibre's `GeolocateControl`. requires HTTPS or localhost; renders disabled if the user denies permission. default false |
| interactive | boolean | enable drag/zoom; set false for static thumbnails. default true |

## Theme & tiles

- Default tiles come from OpenFreeMap (`tiles.openfreemap.org`) — free, no API key, light + dark variants.
- Pass `style` (URL or full `StyleSpecification`) to use MapTiler / Mapbox / a self-hosted style instead.
- Default geometry colors read from the `--primary` CSS variable so maps tonally match the rest of the UI.

## RTL

MapLibre's RTL text plugin is registered automatically on first `<Map>` mount, so Arabic place names render with correct shaping. No per-app wiring needed.

## Cross-platform

This is the **web** component. The native Rahhal apps render maps with iOS MapKit / Android Maps SDK; prop names here intentionally mirror those native concepts (`lngLat`, `zoom`, `pitch`, `bearing`) so designers can speak about either side without translation.
