{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "leaflet-threat-map-radar",
  "title": "Leaflet Threat Map Radar",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/leaflet-threat-map-radar/leaflet-threat-map-radar.component.ts",
      "content": "// Live threat radar on free OpenStreetMap/Esri dark tiles — no API key. Attack arcs hit\n// edge PoPs under a rotating sweep; click an origin to isolate its vector. Port of the React block 1:1.\nimport { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'\nimport { cn } from '@/lib/utils'\nimport { UiBadgeComponent } from '@/ui/badge/badge.component'\nimport {\n  UiCardComponent,\n  UiCardContentComponent,\n  UiCardHeaderComponent,\n  UiCardTitleComponent,\n} from '@/ui/card/card.component'\nimport {\n  UiLeafletMapComponent,\n  UiLeafletMarkerComponent,\n  UiLeafletPolylineComponent,\n} from '@/ui/leaflet-map/leaflet-map.component'\n\nexport type AttackVectorType = 'volumetric' | 'l7' | 'botnet'\nexport type MitigationAction = 'DROP' | 'CHALLENGE' | 'RATE_LIMIT'\n\nexport interface ThreatOrigin {\n  id: string\n  country: string\n  countryCode: string\n  volume: string\n  vectorType: AttackVectorType\n  mitigation: MitigationAction\n  /** [lng, lat] */\n  lngLat: [number, number]\n  targetEdgeId: string\n}\n\nexport interface EdgePoP {\n  id: string\n  code: string\n  city: string\n  /** [lng, lat] */\n  lngLat: [number, number]\n}\n\nexport interface AttackArc {\n  id: string\n  originId: string\n  edgeId: string\n  vectorType: AttackVectorType\n  /** Sampled arc path as [lng, lat][]. */\n  path: [number, number][]\n}\n\nexport const defaultOrigins: ThreatOrigin[] = [\n  {\n    id: 'origin-1',\n    country: 'China',\n    countryCode: 'CN',\n    volume: '642.5K/s',\n    vectorType: 'l7',\n    mitigation: 'DROP',\n    lngLat: [104.2, 35.9],\n    targetEdgeId: 'edge-nrt',\n  },\n  {\n    id: 'origin-2',\n    country: 'Russia',\n    countryCode: 'RU',\n    volume: '418.2K/s',\n    vectorType: 'l7',\n    mitigation: 'CHALLENGE',\n    lngLat: [37.6, 55.7],\n    targetEdgeId: 'edge-fra',\n  },\n  {\n    id: 'origin-3',\n    country: 'Brazil',\n    countryCode: 'BR',\n    volume: '285.4K/s',\n    vectorType: 'botnet',\n    mitigation: 'RATE_LIMIT',\n    lngLat: [-51.9, -14.2],\n    targetEdgeId: 'edge-iad',\n  },\n  {\n    id: 'origin-4',\n    country: 'Vietnam',\n    countryCode: 'VN',\n    volume: '194.0K/s',\n    vectorType: 'volumetric',\n    mitigation: 'DROP',\n    lngLat: [105.8, 21.0],\n    targetEdgeId: 'edge-sin',\n  },\n]\n\nexport const edgeNodes: EdgePoP[] = [\n  { id: 'edge-iad', code: 'IAD', city: 'Ashburn', lngLat: [-77.45, 39.04] },\n  { id: 'edge-fra', code: 'FRA', city: 'Frankfurt', lngLat: [8.68, 50.11] },\n  { id: 'edge-nrt', code: 'NRT', city: 'Tokyo', lngLat: [139.69, 35.69] },\n  { id: 'edge-sin', code: 'SIN', city: 'Singapore', lngLat: [103.85, 1.29] },\n  { id: 'edge-gru', code: 'GRU', city: 'São Paulo', lngLat: [-46.63, -23.55] },\n]\n\n// Curved attack arc: sample a quadratic bezier whose control point bows\n// perpendicular to the route (biased northward) for a great-circle feel.\nexport function arcPath(from: [number, number], to: [number, number], bend = 0.22): [number, number][] {\n  const dx = to[0] - from[0]\n  const dy = to[1] - from[1]\n  const dist = Math.hypot(dx, dy) || 1\n  let nx = -dy / dist\n  let ny = dx / dist\n  if (ny < 0) {\n    nx = -nx\n    ny = -ny\n  }\n  const cx = (from[0] + to[0]) / 2 + nx * dist * bend\n  const cy = (from[1] + to[1]) / 2 + ny * dist * bend\n  const points: [number, number][] = []\n  const steps = 32\n  for (let i = 0; i <= steps; i++) {\n    const t = i / steps\n    const u = 1 - t\n    points.push([u * u * from[0] + 2 * u * t * cx + t * t * to[0], u * u * from[1] + 2 * u * t * cy + t * t * to[1]])\n  }\n  return points\n}\n\nexport const attackArcs: AttackArc[] = [\n  {\n    id: 'arc-1',\n    originId: 'origin-1',\n    edgeId: 'edge-nrt',\n    vectorType: 'l7',\n    path: arcPath([104.2, 35.9], [139.69, 35.69], 0.3),\n  },\n  {\n    id: 'arc-2',\n    originId: 'origin-1',\n    edgeId: 'edge-iad',\n    vectorType: 'l7',\n    path: arcPath([104.2, 35.9], [-77.45, 39.04], 0.22),\n  },\n  {\n    id: 'arc-3',\n    originId: 'origin-2',\n    edgeId: 'edge-fra',\n    vectorType: 'l7',\n    path: arcPath([37.6, 55.7], [8.68, 50.11], 0.35),\n  },\n  {\n    id: 'arc-4',\n    originId: 'origin-3',\n    edgeId: 'edge-iad',\n    vectorType: 'botnet',\n    path: arcPath([-51.9, -14.2], [-77.45, 39.04], 0.25),\n  },\n  {\n    id: 'arc-5',\n    originId: 'origin-4',\n    edgeId: 'edge-sin',\n    vectorType: 'volumetric',\n    path: arcPath([105.8, 21.0], [103.85, 1.29], 0.35),\n  },\n]\n\n// `hex` feeds Leaflet path options; `dot`/`ping` feed the div-icon markers.\nexport const VECTOR_STYLES: Record<AttackVectorType, { hex: string; dot: string; ping: string }> = {\n  volumetric: { hex: '#f43f5e', dot: 'bg-destructive', ping: 'bg-destructive' },\n  l7: { hex: '#f59e0b', dot: 'bg-warning', ping: 'bg-warning' },\n  botnet: { hex: '#0ea5e9', dot: 'bg-info', ping: 'bg-info' },\n}\n\n// Module-level map camera: stable reference so the map center input never re-fires.\nconst MAP_CENTER: [number, number] = [10, 30]\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-leaflet-threat-map-radar, [ui-leaflet-threat-map-radar]',\n  standalone: true,\n  host: { '[attr.class]': '\"contents\"' },\n  imports: [\n    UiBadgeComponent,\n    UiCardComponent,\n    UiCardContentComponent,\n    UiCardHeaderComponent,\n    UiCardTitleComponent,\n    UiLeafletMapComponent,\n    UiLeafletMarkerComponent,\n    UiLeafletPolylineComponent,\n  ],\n  template: `\n    <div data-slot=\"leaflet-threat-map-radar\" [class]=\"rootClass\">\n      <div class=\"flex flex-wrap items-center justify-between gap-3\">\n        <div class=\"flex flex-wrap items-center gap-2\">\n          <h2 class=\"text-foreground text-lg font-semibold tracking-tight\">Threat Radar</h2>\n          <span ui-badge variant=\"outline\" class=\"border-success/30 bg-success/10 text-success text-xs font-medium\">\n            Live\n          </span>\n        </div>\n        <p class=\"text-muted-foreground text-xs\">{{ origins().length }} origins · {{ nodes.length }} edge PoPs</p>\n      </div>\n\n      <div class=\"grid min-h-0 flex-1 grid-cols-1 gap-4 lg:grid-cols-12\">\n        <div ui-card class=\"border-border flex min-h-0 flex-col overflow-hidden shadow-xs lg:col-span-8\">\n          <div class=\"relative min-h-[360px] flex-1 overflow-hidden bg-zinc-950\">\n            <ui-leaflet-map\n              variant=\"dark\"\n              [center]=\"mapCenter\"\n              [zoom]=\"2\"\n              [minZoom]=\"2\"\n              [navigation]=\"false\"\n              [scrollWheelZoom]=\"false\"\n              class=\"pointer-events-none absolute inset-0 size-full\"\n            >\n              <!-- Radar sweep: a rotating conic-gradient wedge layered over the tiles, plus two static range rings. -->\n              <div class=\"pointer-events-none absolute inset-0 z-[500] flex items-center justify-center\">\n                <div class=\"border-success/15 absolute size-[340px] rounded-full border\"></div>\n                <div class=\"border-success/15 absolute size-[220px] rounded-full border\"></div>\n                <div\n                  class=\"size-[340px] animate-[spin_9s_linear_infinite] rounded-full bg-[conic-gradient(from_0deg,rgba(16,185,129,0.35),transparent_75deg)]\"\n                ></div>\n              </div>\n\n              @for (arc of filteredArcs(); track arc.id) {\n                <ui-leaflet-polyline\n                  [lngLatPath]=\"arc.path\"\n                  [color]=\"arcColor(arc)\"\n                  [weight]=\"2\"\n                  dashArray=\"6 4\"\n                  lineCap=\"round\"\n                  [opacity]=\"0.9\"\n                />\n              }\n\n              @for (edge of nodes; track edge.id) {\n                <ui-leaflet-marker [lngLat]=\"edge.lngLat\" anchor=\"center\">\n                  <div class=\"flex flex-col items-center gap-1\">\n                    <span class=\"bg-success block size-2.5 rotate-45 border border-white/70 shadow-xs\"></span>\n                    <span class=\"text-success font-mono text-xs font-bold\">{{ edge.code }}</span>\n                  </div>\n                </ui-leaflet-marker>\n              }\n\n              @for (origin of origins(); track origin.id) {\n                <ui-leaflet-marker [lngLat]=\"origin.lngLat\" anchor=\"center\">\n                  <button\n                    type=\"button\"\n                    class=\"pointer-events-auto relative flex flex-col items-center gap-1\"\n                    [attr.aria-label]=\"origin.country\"\n                    (click)=\"selectOrigin(origin.id)\"\n                  >\n                    <span class=\"font-mono text-xs font-bold text-white\">{{ origin.countryCode }}</span>\n                    <span class=\"relative flex size-3 items-center justify-center\">\n                      <span [class]=\"pingClass(origin)\"></span>\n                      <span [class]=\"dotClass(origin)\"></span>\n                    </span>\n                  </button>\n                </ui-leaflet-marker>\n              }\n            </ui-leaflet-map>\n          </div>\n        </div>\n\n        <div ui-card class=\"border-border flex min-h-0 flex-col overflow-hidden shadow-xs lg:col-span-4\">\n          <div ui-card-header class=\"border-border border-b p-4\">\n            <h3 ui-card-title class=\"text-sm font-semibold\">Origins</h3>\n          </div>\n          <div ui-card-content class=\"divide-border min-h-0 flex-1 divide-y overflow-auto p-0\">\n            @for (origin of origins(); track origin.id) {\n              <button type=\"button\" [class]=\"rowClass(origin)\" (click)=\"selectOrigin(origin.id)\">\n                <div class=\"min-w-0\">\n                  <p class=\"text-foreground truncate text-sm font-medium\">\n                    {{ origin.countryCode }} · {{ origin.country }}\n                  </p>\n                  <p class=\"text-muted-foreground font-mono text-xs tabular-nums\">{{ origin.volume }}</p>\n                </div>\n                <span ui-badge [variant]=\"mitigationVariant(origin.mitigation)\" class=\"shrink-0 text-xs uppercase\">\n                  {{ origin.mitigation.replace('_', ' ') }}\n                </span>\n              </button>\n            }\n          </div>\n        </div>\n      </div>\n    </div>\n  `,\n})\nexport class UiLeafletThreatMapRadarComponent {\n  @Input('origins') set originsInput(v: ThreatOrigin[] | undefined) {\n    this.origins.set(v ?? defaultOrigins.map((o) => ({ ...o, lngLat: [...o.lngLat] as [number, number] })))\n  }\n  @Input('class') className?: string\n\n  readonly origins = signal<ThreatOrigin[]>(defaultOrigins.map((o) => ({ ...o })))\n  readonly selectedId = signal<string | null>(null)\n  protected readonly nodes = edgeNodes\n  protected readonly mapCenter = MAP_CENTER\n\n  readonly filteredArcs = computed(() => {\n    const sel = this.selectedId()\n    if (!sel) return attackArcs\n    return attackArcs.filter((arc) => arc.originId === sel)\n  })\n\n  get rootClass(): string {\n    return cn('flex h-full min-h-0 flex-col gap-4', this.className)\n  }\n\n  selectOrigin(id: string): void {\n    this.selectedId.update((cur) => (cur === id ? null : id))\n  }\n\n  mitigationVariant(action: MitigationAction): 'destructive' | 'warning' | 'info' {\n    if (action === 'DROP') return 'destructive'\n    if (action === 'CHALLENGE') return 'warning'\n    return 'info'\n  }\n\n  arcColor(arc: AttackArc): string {\n    return VECTOR_STYLES[arc.vectorType].hex\n  }\n\n  pingClass(origin: ThreatOrigin): string {\n    return cn('absolute size-3 animate-ping rounded-full opacity-70', VECTOR_STYLES[origin.vectorType].ping)\n  }\n\n  dotClass(origin: ThreatOrigin): string {\n    return cn('relative size-3 rounded-full border-2 border-white/80', VECTOR_STYLES[origin.vectorType].dot)\n  }\n\n  rowClass(origin: ThreatOrigin): string {\n    return cn(\n      'hover:bg-accent/50 flex w-full items-center justify-between gap-3 px-4 py-3 text-left transition-colors',\n      this.selectedId() === origin.id && 'bg-accent/60',\n    )\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/leaflet-threat-map-radar/leaflet-threat-map-radar.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/leaflet-threat-map-radar/index.ts",
      "content": "export {\n  UiLeafletThreatMapRadarComponent,\n  defaultOrigins,\n  edgeNodes,\n  attackArcs,\n  VECTOR_STYLES,\n  arcPath,\n  type AttackVectorType,\n  type MitigationAction,\n  type ThreatOrigin,\n  type EdgePoP,\n  type AttackArc,\n} from './leaflet-threat-map-radar.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/leaflet-threat-map-radar/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/badge.json",
    "https://uipkge.dev/r/angular/card.json",
    "https://uipkge.dev/r/angular/leaflet-map.json"
  ],
  "description": "Live threat radar on free OpenStreetMap/Esri dark tiles — no API key. Attack arcs hit edge PoPs under a rotating sweep; click an origin to isolate its vector.",
  "categories": [
    "security",
    "dashboard",
    "devops"
  ]
}