Sign-in travel
An account's sign-ins joined by legs on a map, with the time and distance between them. A move faster than travel allows tears apart.
Your data
Pass your sign-ins as signIns. Without it the component shows six invented demo sign-ins. See Your data.
Playground
Change a prop and the component re-renders. Props marked remounts set an initial value, so the component starts over.
<GeoAccess maxKmh={1000} minKm={500} unit="km" replay timeZone="UTC" />Installation
pnpm dlx shadcn@latest add @lumesec/geo-accessFirst timeRegister the @lumesec registry once, or install by URL+
Adds @lumesec to your components.json:
pnpm dlx shadcn@latest registry add @lumesec=https://elements.lumesec.ai/r/{name}.jsonOr skip the setup and install by URL:
npx shadcn@latest add https://elements.lumesec.ai/r/geo-access.jsonUsage
import { GeoAccess, type SignIn } from "@/components/lumesec/geo-access";
export function Example({ signIns }: { signIns: SignIn[] }) {
return <GeoAccess signIns={signIns} timeZone="Europe/London" onFlag={(flag) => console.log(flag.from, flag.to, flag.kmh)} />;
}Behaviour
An account's sign-ins in order, joined by lines on a map, with the speed each move would have needed. A move faster than travel allows is flagged as impossible.
A flagged leg tears: its line splits in the middle, the halves pull apart, red sparks crackle in the gap and a chip shows the speed, such as 16,300 km/h. The list beside the map shows the time and distance between sign-ins. Locations from IP addresses are approximate, so short jumps are never flagged.
API reference
Props
Also accepts every prop of <div> (React.ComponentProps<"div">), spread onto the root element.
signInsTypereadonly SignIn[]Default6 demo sign-insThe account's sign-ins: { id, time, lat, lon, city?, device?, address? }, in any order (they are sorted by time). time is epoch ms, an ISO 8601 string (read as UTC without an offset) or a Date. Entries with a duplicate id, an invalid time or an invalid position are skipped with a development warning. An empty array shows "No sign-ins yet".
maxKmhTypenumberDefault1000Speeds above this, in km/h, are impossible.
minKmTypenumberDefault500Moves shorter than this, in km, are never flagged, since IP locations can be off by a few hundred kilometres.
valueTypestring | nullSelected sign-in id (controlled). Use with onValueChange.
defaultValueTypestring | nullDefaultnullSelected sign-in id when uncontrolled.
onValueChangeType(value: string | null) => voidCalled when a sign-in is selected from the list or the map, or the selection is cleared.
onFlagType(flag: TravelFlag) => voidCalled once per impossible leg when it first appears in the data: { from, to, km, hours, kmh } with the two sign-in ids. kmh is Infinity when both sign-ins share a timestamp.
replayTypebooleanDefaulttruePlays the legs in order on first view and when the data changes. Appended sign-ins play from the first new one.
unitType"km" | "mi"Default"km"Unit of distances and speeds: km with km/h, or mi with mph. maxKmh and minKm stay in km.
timeZoneTypestringDefault"UTC"IANA time zone of the times in the list. Invalid zones fall back to UTC.
localeTypestringBCP 47 locale for numbers and times. Without it, en-US.
refTypeReact.Ref<GeoAccessHandle>Exposes replay().
Ref
ref receives a GeoAccessHandle handle with these methods.
replay(): voidPlays the legs again in order from the first sign-in. Does nothing with reduced motion or replay={false}.
Keyboard
| Keys | Action |
|---|---|
| Tab | Moves into the list, to the selected sign-in or the first one. The list is one tab stop. |
| ArrowDown / ArrowUp | Moves focus to the next or previous sign-in. |
| Home / End | Moves focus to the first or last sign-in. |
| Enter / Space | Selects the focused sign-in, or clears it when it is already selected. |
| Escape | Clears the selection. |
Accessibility
- The root is a group labelled "Sign-in travel". The timeline is an ordered list labelled "Sign-ins, oldest first" with one native button per sign-in, using a roving tab index.
- Each button is labelled with the place, the time, the device and the address, carries
aria-pressedfor the selection and is described by the leg that leads to it, for example "40 minutes later, 10,880 km away: impossible travel at 16,320 km/h." - A polite live region announces each impossible leg once, for example "Impossible travel: London to Singapore, 10,880 km in 40 minutes."
- The map (canvases, markers, place names and speed chips) is
aria-hidden; the list carries the same data. Markers respond to the pointer only. - Reduced motion: the land shows without its reveal, the legs appear complete without the replay, and impossible legs show a static dashed red gap with their chip, without the wobble or sparks. Rows do not flash 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.
This component reads --lumesec, --lumesec-soft, --lumesec-glint, --lumesec-shine, --destructive, --card, --card-foreground, --muted, --muted-foreground, --foreground, --background, --border, --lumesec-info, --lumesec-success and --lumesec-warning.
Notes
- Each leg is checked with the great-circle distance and the time between the two sign-ins: it is impossible when it covers more than
minKmat more thanmaxKmh. Sign-ins within 25 km share one marker, with a count; a leg between them takes no time in the replay. - The map is Equal Earth, centred on the sign-ins and zoomed until they fill the box with a margin; a single sign-in shows about 70 degrees around it. Calm legs bend slightly to the left of travel, so a return trip runs beside the outward one.
- The replay draws each leg in 0.6 s with a packet at its head. A point pings the land dots around it as it is reached and its row lights up. An impossible leg draws to its midpoint, tears there and opens a 7 px gap on a spring; red sparks crackle in the gap for 1.4 s, the chip with the speed and the time pops in, and the far half grows back from the destination.
- Selecting a sign-in, in the list or on the map, draws its incoming and outgoing legs larger, dims the rest and shows the incoming route and speed under the map. Selecting an end of a torn leg crackles its gap again. Clicking a marker repeatedly steps through the sign-ins at that place; clicking the empty map clears the selection.
- Times in the list use
formatZonedTimewith a short date. The time between sign-ins is written withminfor minutes, so it does not read as metres next to a distance.
Related
Was this page helpful?