A world map of login attempts with volume-sized city dots and destructive ping rings flagging anomalous sign-in locations.
npx shadcn@latest add @paragon/login-activity-mapAlso installs: world-map
"use client";
import * as React from "react";
import { useInView, useReducedMotion } from "motion/react";
import { ShieldAlert } from "lucide-react";
import { cn } from "@/lib/utils";
import { WorldMap, type MapMarker } from "@/registry/paragon/ui/world-map";
export interface LoginPoint {
id: string;
/** Longitude -180..180. */
lng: number;
/** Latitude -90..90. */
lat: number;
city: string;
/** Login attempts from here. */
count: number;
/** Flags an impossible-travel / anomalous location. */
anomaly?: boolean;
}
export interface LoginActivityMapProps extends React.ComponentProps<"div"> {
points?: LoginPoint[];
/** Accent color for benign login markers. Defaults to the primary token. */
accent?: string;
/** Disables the staggered dot enter and anomaly ping. */
static?: boolean;
}
const DEFAULT_POINTS: LoginPoint[] = [
{ id: "sf", lng: -122.4, lat: 37.8, city: "San Francisco", count: 412 },
{ id: "nyc", lng: -74.0, lat: 40.7, city: "New York", count: 388 },
{ id: "ldn", lng: -0.1, lat: 51.5, city: "London", count: 201 },
{ id: "ber", lng: 13.4, lat: 52.5, city: "Berlin", count: 96 },
{ id: "sgp", lng: 103.8, lat: 1.35, city: "Singapore", count: 54 },
{ id: "syd", lng: 151.2, lat: -33.9, city: "Sydney", count: 33 },
{ id: "lag", lng: 3.4, lat: 6.5, city: "Lagos", count: 3, anomaly: true },
];
/**
* A world map of login attempts with anomaly flags, built on the accurate
* `world-map` primitive (real country geometry + geographic projection). Cities
* plot as `markers` positioned by lng/lat; the primitive projects them onto the
* correct landmass and renders a hover tooltip (city + attempt count).
* Anomalous locations use the destructive tone with a pulsing ring. The map
* reveals with a soft house enter on first view, and lists each anomaly as an
* accessible callout. Deterministic geometry — no random or time-based values.
*/
export function LoginActivityMap({
points = DEFAULT_POINTS,
accent,
static: isStatic = false,
className,
...props
}: LoginActivityMapProps) {
const ref = React.useRef<HTMLDivElement>(null);
const reduced = useReducedMotion();
const inView = useInView(ref, { once: true, margin: "0px 0px -24px 0px" });
const animate = !isStatic && !reduced;
const revealed = !animate || inView;
const anomalies = React.useMemo(
() => points.filter((p) => p.anomaly),
[points],
);
const total = React.useMemo(
() => points.reduce((s, p) => s + p.count, 0),
[points],
);
const markers = React.useMemo<MapMarker[]>(
() =>
points.map((p) => ({
lng: p.lng,
lat: p.lat,
label: p.city,
value: `${p.count} attempt${p.count === 1 ? "" : "s"}`,
tone: p.anomaly ? ("destructive" as const) : ("accent" as const),
pulse: p.anomaly,
})),
[points],
);
return (
<div
ref={ref}
data-slot="login-activity-map"
className={cn(
"w-full max-w-md rounded-xl bg-card p-4 text-card-foreground shadow-border",
className,
)}
{...props}
>
<style href="paragon-login-activity-map" precedence="paragon">{`
[data-login-map-canvas] {
transition-property: opacity, transform, filter;
transition-duration: var(--duration-slow, 500ms);
transition-timing-function: var(--ease-out);
}
@media (prefers-reduced-motion: reduce) {
[data-login-map-canvas] {
transition: none !important;
opacity: 1 !important;
transform: none !important;
filter: none !important;
}
}
`}</style>
<div className="mb-3 flex items-center justify-between gap-3">
<h3 className="min-w-0 truncate text-sm font-semibold">
Login activity
</h3>
<span className="shrink-0 text-xs text-muted-foreground tabular-nums">
{total.toLocaleString()} attempts · 24h
</span>
</div>
<div
data-login-map-canvas
style={{
opacity: revealed ? 1 : 0,
transform: revealed ? "translateY(0)" : "translateY(12px)",
filter: revealed ? "blur(0)" : "blur(4px)",
}}
>
<WorldMap
accent={accent}
markers={markers}
className="[--pg-map-land:color-mix(in_oklch,var(--color-foreground)_8%,transparent)]"
aria-label={
`Login attempts across ${points.length} cities. ` +
(anomalies.length
? `${anomalies.length} anomalous location${
anomalies.length === 1 ? "" : "s"
}: ${anomalies.map((a) => a.city).join(", ")}.`
: "No anomalies.")
}
/>
</div>
{anomalies.length > 0 ? (
<ul className="mt-3 space-y-1.5 border-t pt-3">
{anomalies.map((a) => (
<li key={a.id} className="flex items-center gap-2 text-[13px]">
<ShieldAlert
aria-hidden
className="size-3.5 shrink-0 text-destructive"
/>
<span className="shrink-0 font-medium text-destructive">
Anomalous sign-in
</span>
<span className="min-w-0 truncate text-muted-foreground">
{a.city} · {a.count} attempt{a.count === 1 ? "" : "s"}
</span>
</li>
))}
</ul>
) : (
<p className="mt-3 border-t pt-3 text-[13px] text-muted-foreground">
No anomalous sign-in locations detected.
</p>
)}
</div>
);
}