# Attack origins
> Inbound requests converge on a protected site at the centre of a dotted globe. Blocked ones dent its shield ring and burst off it.
- React: `import { GeoThreats } from "@/components/lumesec/geo-threats"`
- 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-threats.json
- Page: https://elements.lumesec.ai/components/location/threats



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

Demo source:

```tsx
"use client";

import * as React from "react";

import { GeoThreats, type GeoThreatsHandle } from "@/components/lumesec/geo-threats";
import { createRandom, findCity, scatterAround } from "@/lib/lumesec/geo-data";

const BUTTON =
  "h-[30px] cursor-pointer rounded-lg border border-border bg-card px-[11px] 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";

/** A flood comes from two places at once, around São Paulo and around Delhi (invented demo traffic). */
const FLOOD_SOURCES = ["gru", "del"] as const;
const FLOOD_SIZE = 40;
const FLOOD_MS = 2000;
const FLOOD_SPREAD_KM = 240;

export default function GeoThreatsDemo() {
  const threats = React.useRef<GeoThreatsHandle>(null);
  const timers = React.useRef(new Set<number>());
  const round = React.useRef(0);

  React.useEffect(() => {
    const pending = timers.current;
    return () => {
      for (const timer of pending) window.clearTimeout(timer);
      pending.clear();
    };
  }, []);

  /** Pushes 40 blocked requests from the two sources over 2 s, alternating between them. */
  function flood() {
    const wave = round.current++;
    const random = createRandom(`flood-${wave}`);
    for (let i = 0; i < FLOOD_SIZE; i++) {
      const timer = window.setTimeout(
        () => {
          timers.current.delete(timer);
          const city = findCity(FLOOD_SOURCES[i % FLOOD_SOURCES.length]);
          if (!city) return;
          const origin = scatterAround(random, city, FLOOD_SPREAD_KM, true);
          threats.current?.push({ id: `flood-${wave}-${i}`, lat: origin.lat, lon: origin.lon, verdict: "blocked" });
        },
        (i * FLOOD_MS) / FLOOD_SIZE,
      );
      timers.current.add(timer);
    }
  }

  return (
    <div className="grid w-full max-w-[660px] gap-3">
      <GeoThreats ref={threats} />
      <div className="flex flex-wrap items-center gap-2">
        <button type="button" className={BUTTON} onClick={flood}>
          Flood
        </button>
        <span className="ml-auto text-[12px] text-muted-foreground">Demo data, invented origins</span>
      </div>
    </div>
  );
}
```

> **Your data:** Pass your requests as `events`, or call `push()` on the ref for a live feed. Without `events` the component shows a seeded demo stream with invented origins and prefilled demo counts. 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-threats
```

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-threats.json
```

## Usage

React:

```tsx
import { GeoThreats, type ThreatEvent } from "@/components/lumesec/geo-threats";

export function Example({ requests }: { requests: ThreatEvent[] }) {
  return <GeoThreats target={{ label: "eu-central", lat: 50.11, lon: 8.68 }} events={requests} />;
}
```

## Behaviour

Inbound requests from around the world converge on one protected site at the centre of a globe; requests from the far side come up over the horizon. A ring of dots around the site is its shield: blocked requests hit the ring and burst off it, allowed requests pass through and land.

Where a request hits, the shield dents and lights up for a moment. Counts per verdict and the top origin regions roll beside the globe; arcs in flight are capped and the rest are folded into the counts.

## API reference

### Props

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

| Prop            | Type                           | Default                                              | Description                                                                                                                                                                                                                                               |
| --------------- | ------------------------------ | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `target`        | `ThreatTarget`                 | `{ label: "eu-central", lat: 50.1109, lon: 8.6821 }` | `{ label, lat, lon }` of the protected site. The globe faces it, so it sits at the centre of the disc; the label shows in the header and the accessible summary.                                                                                          |
| `events`        | `readonly ThreatEvent[]`       | —                                                    | Requests: `{ id, lat, lon, verdict, region?, time? }` with `verdict` `blocked` or `allowed`. Each new `id` animates once, so the array can grow or be replaced. Without it a seeded demo stream runs; pass an empty array to show only what you `push()`. |
| `maxInFlight`   | `number`                       | `24`                                                 | Arcs drawn at once. Further requests go straight into the counts.                                                                                                                                                                                         |
| `windowSeconds` | `number`                       | `60`                                                 | Window for the counts and the origin list, in seconds. Requests older than the window are ignored.                                                                                                                                                        |
| `shieldRadius`  | `number`                       | `26`                                                 | Radius of the shield ring in px, from 10 to 120 and at most 60% of the globe's radius.                                                                                                                                                                    |
| `live`          | `boolean`                      | `true`                                               | Runs the demo stream while `events` is not given. Off keeps the prefilled demo counts without new requests.                                                                                                                                               |
| `onEvent`       | `(event: ThreatEvent) => void` | —                                                    | Called when a request reaches the shield (blocked) or the site (allowed). Requests without an arc call it at once.                                                                                                                                        |
| `ref`           | `React.Ref<GeoThreatsHandle>`  | —                                                    | Exposes `push()`.                                                                                                                                                                                                                                         |

### Ref

`ref` receives a `GeoThreatsHandle` handle with these methods.

| Method                           | Description                                                                         |
| -------------------------------- | ----------------------------------------------------------------------------------- |
| `push(event: ThreatEvent): void` | Adds one request, with or without `events`. An id that was already seen is ignored. |

## Accessibility

* The globe stage has `role="img"` and an `aria-label` that summarises the window, for example "Requests to eu-central. In the last minute: 1,284 blocked, 312 allowed, most from Southern Asia." It is refreshed at most every 15 seconds.
* The counters and the origin list are real text. Each origin row has a visually hidden sentence with its region, total, blocked and allowed counts; the rolling digits and the dotted bars are `aria-hidden`.
* Both canvases, the server placeholder and the target square are `aria-hidden`. The globe takes no pointer or keyboard input.
* Reduced motion: no arcs, dents or sparks play: each request lights the shield dots facing its origin for 400 ms and is counted at once, the counts swap without rolling and the origin rows move without sliding.

## 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-glint`, `--lumesec-shine`, `--lumesec-success`, `--destructive`, `--foreground`, `--muted-foreground`, `--card` and `--border`.

## Notes

* The globe always faces the target and never turns, so each arc is projected once. Arcs rise higher the farther they travel; origins on the far side climb over the horizon, where a short glint marks the arc.
* Packets speed up toward the site: 1 s plus up to 0.8 s for the far side of the world. A blocked packet stops at the ring: the dots around the impact dent inward on springs, heat up and throw ten sparks back the way it came. An allowed packet parts the ring, passes and lands with a small ring of lit dots around the site.
* Requests are grouped by `region`. Without it, the group is the world region of the origin (such as `Southern Asia` or `South America`) from the embedded country data; points at sea take the nearest country's region.
* Counts cover the last `windowSeconds`, by each request's `time` (or when it arrived). The origin list shows the five regions with the most requests and re-sorts at most once a second; each row has a dotted bar with the region's share of all requests, blocked dots first.
* Requests older than 10 seconds, requests that arrive while the globe is off screen or the tab is hidden, and requests beyond `maxInFlight` are counted without an arc. Ids are remembered (the last 4,000), so a re-render with the same array never counts them twice.
* The stage is square, at most 420 px wide; at 560 px of container width and above the origin list stands beside it, else below. The arcs and sparks draw on a canvas that reaches 40 px past the stage on every side.
* The server markup shows the disc with a 30° graticule, the shield ring and the target; it fades to the dotted globe after the first frame. The animation loop pauses while the globe is off screen and stops when nothing moves.
* The demo stream sends about four requests a second, 80% of them blocked, from invented sources around real cities, and prefills a window of counts on mount.


