# Geofence
> Geofence zones as true geodesic circles on a dot map. A device that leaves every zone plucks the fence it crossed, and a wave runs around it.
- React: `import { GeoGeofence } from "@/components/lumesec/geo-geofence"`
- Collection: Location (https://elements.lumesec.ai/components/location)
- Data: takes your data (`data`); shows demo data until you pass it
- Registry item: https://elements.lumesec.ai/r/geo-geofence.json
- Page: https://elements.lumesec.ai/components/location/geofence



Live preview: https://elements.lumesec.ai/view/geo-geofence

Demo source:

```tsx
"use client";

import * as React from "react";

import { GeoGeofence, type GeofenceZone } from "@/components/lumesec/geo-geofence";

/** The two demo zones: 1,200 km around Paris and 900 km around New York. */
const INITIAL_ZONES: readonly GeofenceZone[] = [
  { id: "western-europe", label: "Western Europe", center: { lat: 48.8566, lon: 2.3522 }, radiusKm: 1200 },
  { id: "east-coast", label: "East coast", center: { lat: 40.7128, lon: -74.006 }, radiusKm: 900 },
];

/** Smallest radius the Shrink button goes down to, in km. */
const SHRINK_FLOOR_KM = 150;

const BUTTON =
  "h-[30px] cursor-pointer rounded-lg border border-border bg-card px-[11px] font-[inherit] text-[12.5px] font-medium text-foreground outline-none hover:border-[color-mix(in_srgb,var(--lumesec)_50%,var(--border))] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-lumesec disabled:cursor-default disabled:opacity-50 disabled:hover:border-border";

/** Each press takes a zone to 55 % of its radius, rounded to 10 km, so trackers near the fence fall outside. */
function shrinkZone(zone: GeofenceZone): GeofenceZone {
  const radiusKm = Math.max(SHRINK_FLOOR_KM, Math.round((zone.radiusKm * 0.55) / 10) * 10);
  return { ...zone, radiusKm };
}

export default function GeoGeofenceDemo() {
  const [zones, setZones] = React.useState<readonly GeofenceZone[]>(INITIAL_ZONES);
  const changed = zones !== INITIAL_ZONES;
  const canShrink = zones.some((zone) => zone.radiusKm > SHRINK_FLOOR_KM);

  return (
    <div className="grid w-full max-w-[660px] gap-3">
      <GeoGeofence zones={zones} onZonesChange={setZones} label="Tracker zones" />
      <div className="flex flex-wrap items-center gap-2">
        <button
          type="button"
          onClick={() => setZones((current) => current.map(shrinkZone))}
          disabled={!canShrink}
          className={BUTTON}
        >
          Shrink zones
        </button>
        <button type="button" onClick={() => setZones(INITIAL_ZONES)} disabled={!changed} className={BUTTON}>
          Reset
        </button>
      </div>
    </div>
  );
}
```

> **Your data:** Pass your tracked devices as `devices` and your zones as `zones` or `defaultZones`. Without `devices` the component shows six demo trackers on a simulated walk. See [Your data](/docs/data).

## Playground

Change a prop and the component re-renders. Props marked remounts set an initial value, so the component starts over.

## Installation

```bash
npx shadcn@latest add @lumesec/geo-geofence
```

First time with the @lumesec registry? Register it once, or install by URL:

```bash
npx shadcn@latest registry add @lumesec=https://elements.lumesec.ai/r/{name}.json
npx shadcn@latest add https://elements.lumesec.ai/r/geo-geofence.json
```

## Usage

React:

```tsx
import * as React from "react";

import { GeoGeofence, type GeofenceZone, type TrackedDevice } from "@/components/lumesec/geo-geofence";

const initialZones: GeofenceZone[] = [
  { id: "depot", label: "Depot region", center: { lat: 52.52, lon: 13.405 }, radiusKm: 400 },
];

export function Example({ vehicles }: { vehicles: TrackedDevice[] }) {
  const [zones, setZones] = React.useState<readonly GeofenceZone[]>(initialZones);
  return (
    <GeoGeofence
      zones={zones}
      onZonesChange={setZones}
      devices={vehicles}
      onBreach={({ device, zone }) => console.log(`${device.label} left ${zone.label}`)}
    />
  );
}
```

## Behaviour

Allowed zones as true geodesic circles on a flat map, with tracked devices inside them. Drag a zone's centre to move it and the handle on its edge to resize it; the circle keeps its real shape, so it stretches toward the poles.

When a device leaves every zone, the fence it crossed rings like a plucked string: a wave runs around the circle from the crossing point in both directions, and the device turns to a warning colour.

## API reference

### Props

Also accepts every prop of `<div>` (`React.ComponentProps<"div">`), spread onto the root element.

| Prop            | Type                                       | Default               | Description                                                                                                                                                                                                                                                                 |
| --------------- | ------------------------------------------ | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `zones`         | `readonly GeofenceZone[]`                  | —                     | The zones (controlled): `{ id, label, center: { lat, lon }, radiusKm }`. Use with `onZonesChange`. A radius that would put a pole inside the circle is drawn and checked at the largest radius that does not.                                                               |
| `defaultZones`  | `readonly GeofenceZone[]`                  | `2 demo zones`        | The zones when uncontrolled. The default is Western Europe (1,200 km around Paris) and East coast (900 km around New York).                                                                                                                                                 |
| `onZonesChange` | `(zones: readonly GeofenceZone[]) => void` | —                     | Called with every zone when a drag ends or a key press moves or resizes a zone.                                                                                                                                                                                             |
| `onZonesInput`  | `(zones: readonly GeofenceZone[]) => void` | —                     | Called with every zone on each pointer move while a zone is dragged or resized.                                                                                                                                                                                             |
| `devices`       | `readonly TrackedDevice[]`                 | `6 demo trackers`     | The tracked devices: `{ id, label?, lat, lon }`. When a position changes, the device glides there in about half a second and is checked against the zones on the way. Without it, six demo trackers walk slowly in and out of the zones. An empty array shows no devices.   |
| `editable`      | `boolean`                                  | `true`                | Shows the centre and radius handles, so zones can be moved and resized.                                                                                                                                                                                                     |
| `minRadiusKm`   | `number`                                   | `50`                  | Smallest radius a drag or key press can set, in km.                                                                                                                                                                                                                         |
| `maxRadiusKm`   | `number`                                   | `3000`                | Largest radius a drag or key press can set, in km.                                                                                                                                                                                                                          |
| `onBreach`      | `(event: GeofenceEvent) => void`           | —                     | Called once each time a device leaves every zone: `{ device, zone, point }`, with the zone it left last and the point on the fence where it crossed.                                                                                                                        |
| `onEnter`       | `(event: GeofenceEvent) => void`           | —                     | Called when a device that was outside every zone comes back into one: `{ device, zone, point }`.                                                                                                                                                                            |
| `bounds`        | `GeoBounds`                                | `fitted to the zones` | The part of the world the map shows, `{ west, south, east, north }` in degrees (`east` may exceed 180 across the antimeridian), widened to the map's aspect. Without it the map fits the zones, and fits again when zones are added or removed, never while one is dragged. |
| `label`         | `string`                                   | `"Geofence map"`      | Accessible name of the map.                                                                                                                                                                                                                                                 |

### Ref

`ref` points at the root `HTMLDivElement`.

## Keyboard

| Keys                                         | Action                                                                                                      |
| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| Tab                                          | Moves through each zone's centre handle and radius handle, then into the devices, which share one tab stop. |
| ArrowUp / ArrowDown / ArrowLeft / ArrowRight | On a centre handle: move the zone 0.5° north, south, west or east (Shift: 5°). The centre stays on the map. |
| ArrowRight / ArrowUp, ArrowLeft / ArrowDown  | On a radius handle: add or remove 25 km (Shift: 250 km).                                                    |
| PageUp / PageDown                            | On a radius handle: add or remove 250 km.                                                                   |
| Home / End                                   | On a radius handle: set `minRadiusKm` / `maxRadiusKm`. On a device: focus the first / last device.          |
| ArrowUp / ArrowDown / ArrowLeft / ArrowRight | On a device: focus the nearest device in that direction on the map.                                         |

## Accessibility

* The map is a `role="group"` with `aria-roledescription="map"` and the `label` as its name; the canvases are `aria-hidden`.
* Each centre handle is a native `<button>` with `aria-roledescription="zone centre"` and a name of the zone label and its coordinates, for example "Western Europe, 48.9° N, 2.4° E".
* Each radius handle has `role="slider"` in km, `aria-label` "Radius of Western Europe", `aria-valuemin`, `aria-valuemax`, `aria-valuenow` and an `aria-valuetext` such as "1,200 kilometres". While it has keyboard focus or is dragged, a chip beside it shows the radius.
* Devices are native `<button>` elements whose names say where they are: "Tracker 3, inside Western Europe" or "Tracker 3, outside every zone". Hover or keyboard focus shows a chip with the same text.
* Every breach is announced once in an assertive live region: "Tracker 3 left Western Europe".
* The zone list is a `<table>` with column headers for the zone, its radius and the number of devices inside.
* Handles have a 2 px accent focus ring and a hit area of about 30 px for touch; only the handles block page scrolling while dragged.
* Reduced motion: there is no pluck wave, no sparks and no fence spring: a breach turns the crossed section of the fence to the warning colour for one second, handles move the fence directly, new zones appear at once, devices jump to new positions, and numbers change without rolling.

## Theming

Styled with Tailwind classes on your shadcn theme tokens, so light and dark follow your theme. The accent comes from `--lumesec`. See [Theming](/docs/theming).

This component reads `--lumesec`, `--lumesec-foreground`, `--lumesec-shine`, `--lumesec-success`, `--lumesec-warning`, `--foreground`, `--muted-foreground`, `--muted`, `--card` and `--border`.

## Notes

* Each zone is a real geodesic circle: 128 points at even bearings around the centre, projected as one ring and resampled into fence dots every 4 px. On this equirectangular map it stretches east to west toward the poles.
* A device is outside only when it has left every zone. The zone it left last is the one that rings: two pulses leave the crossing point in opposite directions at 220 px/s, pass through each other on the far side and fade within 1.2 s.
* Containment is checked every frame by great-circle distance, so a breach fires when the device crosses the fence on screen, whether the device moved or the zone was dragged off it.
* With no zones every device counts as outside, without breach events. The first check of a device never fires an event.
* While a zone is dragged, the fence dots spring after the handle: when resizing, the dots near the handle follow fastest, so the ring stretches like rubber.
* Devices outside the shown area are pinned to the map's edge.
* The fence spring, plucks, the demo walk and device glides run only while the map is on screen. With `devices` passed and nothing moving, the animation stops.


