{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "threat-map-radar",
  "title": "Threat Map Radar",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/threat-map-radar/threat-map-radar.component.ts",
      "content": "// Live threat radar over a Mapbox world map. Attack arcs hit edge PoPs; click an origin to\n// isolate its vector. Port of the Vue/React block 1:1 (Mapbox UiMapComponent, same lib as Vue).\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 { UiMapComponent } from '@/ui/map/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  coords: { x: number; y: number }\n  targetEdgeId: string\n}\n\nexport interface EdgePoP {\n  id: string\n  code: string\n  city: string\n  coords: { x: number; y: number }\n}\n\nexport interface AttackArc {\n  id: string\n  originId: string\n  edgeId: string\n  sourceCoords: { x: number; y: number }\n  targetCoords: { x: number; y: number }\n  controlOffset: number\n  vectorType: AttackVectorType\n  gradientId: string\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    coords: { x: 785, y: 185 },\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    coords: { x: 595, y: 135 },\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    coords: { x: 310, y: 340 },\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    coords: { x: 725, y: 275 },\n    targetEdgeId: 'edge-sin',\n  },\n]\n\nexport const edgeNodes: EdgePoP[] = [\n  { id: 'edge-iad', code: 'IAD', city: 'Ashburn', coords: { x: 270, y: 185 } },\n  { id: 'edge-fra', code: 'FRA', city: 'Frankfurt', coords: { x: 515, y: 145 } },\n  { id: 'edge-nrt', code: 'NRT', city: 'Tokyo', coords: { x: 840, y: 185 } },\n  { id: 'edge-sin', code: 'SIN', city: 'Singapore', coords: { x: 755, y: 310 } },\n  { id: 'edge-gru', code: 'GRU', city: 'São Paulo', coords: { x: 345, y: 385 } },\n]\n\nexport const attackArcs: AttackArc[] = [\n  {\n    id: 'arc-1',\n    originId: 'origin-1',\n    edgeId: 'edge-nrt',\n    sourceCoords: { x: 785, y: 185 },\n    targetCoords: { x: 840, y: 185 },\n    controlOffset: -40,\n    vectorType: 'l7',\n    gradientId: 'attack-gradient-amber',\n  },\n  {\n    id: 'arc-2',\n    originId: 'origin-1',\n    edgeId: 'edge-iad',\n    sourceCoords: { x: 785, y: 185 },\n    targetCoords: { x: 270, y: 185 },\n    controlOffset: -160,\n    vectorType: 'l7',\n    gradientId: 'attack-gradient-amber',\n  },\n  {\n    id: 'arc-3',\n    originId: 'origin-2',\n    edgeId: 'edge-fra',\n    sourceCoords: { x: 595, y: 135 },\n    targetCoords: { x: 515, y: 145 },\n    controlOffset: -35,\n    vectorType: 'l7',\n    gradientId: 'attack-gradient-amber',\n  },\n  {\n    id: 'arc-4',\n    originId: 'origin-3',\n    edgeId: 'edge-iad',\n    sourceCoords: { x: 310, y: 340 },\n    targetCoords: { x: 270, y: 185 },\n    controlOffset: -45,\n    vectorType: 'botnet',\n    gradientId: 'attack-gradient-info',\n  },\n  {\n    id: 'arc-5',\n    originId: 'origin-4',\n    edgeId: 'edge-sin',\n    sourceCoords: { x: 725, y: 275 },\n    targetCoords: { x: 755, y: 310 },\n    controlOffset: 30,\n    vectorType: 'volumetric',\n    gradientId: 'attack-gradient-rose',\n  },\n]\n\nexport function computeCurvePath(arc: AttackArc): string {\n  const { sourceCoords, targetCoords, controlOffset } = arc\n  const midX = (sourceCoords.x + targetCoords.x) / 2\n  const midY = (sourceCoords.y + targetCoords.y) / 2 + controlOffset\n  return `M ${sourceCoords.x} ${sourceCoords.y} Q ${midX} ${midY} ${targetCoords.x} ${targetCoords.y}`\n}\n\nexport function originColor(type: AttackVectorType): string {\n  if (type === 'volumetric') return '#f43f5e'\n  if (type === 'l7') return '#f59e0b'\n  return '#0ea5e9'\n}\n\nexport function mitigationVariant(action: MitigationAction): 'destructive' | 'warning' | 'info' {\n  if (action === 'DROP') return 'destructive'\n  if (action === 'CHALLENGE') return 'warning'\n  return 'info'\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-threat-map-radar, [ui-threat-map-radar]',\n  standalone: true,\n  // React renders the root <div> itself: the host stays out of layout and `class` goes to the root.\n  host: { '[attr.class]': '\"contents\"' },\n  imports: [\n    UiBadgeComponent,\n    UiCardComponent,\n    UiCardContentComponent,\n    UiCardHeaderComponent,\n    UiCardTitleComponent,\n    UiMapComponent,\n  ],\n  template: `\n    <div data-slot=\"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 · {{ edges.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            <div\n              ui-map\n              variant=\"dark\"\n              [accessToken]=\"accessToken() ?? ''\"\n              [center]=\"[0, 18]\"\n              [zoom]=\"1.05\"\n              [navigation]=\"false\"\n              class=\"pointer-events-none absolute inset-0 size-full\"\n            ></div>\n            <svg\n              class=\"pointer-events-none absolute inset-0 size-full\"\n              viewBox=\"0 0 1000 500\"\n              fill=\"none\"\n              preserveAspectRatio=\"xMidYMid slice\"\n            >\n              <defs>\n                <linearGradient id=\"attack-gradient-rose\" x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"0%\">\n                  <stop offset=\"0%\" stop-color=\"#f43f5e\" stop-opacity=\"0.95\" />\n                  <stop offset=\"100%\" stop-color=\"#10b981\" stop-opacity=\"0.35\" />\n                </linearGradient>\n                <linearGradient id=\"attack-gradient-amber\" x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"0%\">\n                  <stop offset=\"0%\" stop-color=\"#f59e0b\" stop-opacity=\"0.95\" />\n                  <stop offset=\"100%\" stop-color=\"#38bdf8\" stop-opacity=\"0.35\" />\n                </linearGradient>\n                <linearGradient id=\"attack-gradient-info\" x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"0%\">\n                  <stop offset=\"0%\" stop-color=\"#0ea5e9\" stop-opacity=\"0.95\" />\n                  <stop offset=\"100%\" stop-color=\"#38bdf8\" stop-opacity=\"0.35\" />\n                </linearGradient>\n              </defs>\n\n              @for (arc of filteredArcs(); track arc.id) {\n                <g>\n                  <path\n                    [attr.d]=\"curvePath(arc)\"\n                    fill=\"none\"\n                    [attr.stroke]=\"'url(#' + arc.gradientId + ')'\"\n                    stroke-width=\"2\"\n                    stroke-dasharray=\"6 4\"\n                    stroke-linecap=\"round\"\n                  >\n                    <animate attributeName=\"stroke-dashoffset\" from=\"100\" to=\"0\" dur=\"2.5s\" repeatCount=\"indefinite\" />\n                  </path>\n                  <circle r=\"3\" fill=\"#ffffff\">\n                    <animateMotion [attr.path]=\"curvePath(arc)\" dur=\"2.8s\" repeatCount=\"indefinite\" />\n                  </circle>\n                </g>\n              }\n\n              @for (edge of edges; track edge.id) {\n                <g [attr.transform]=\"'translate(' + edge.coords.x + ', ' + edge.coords.y + ')'\">\n                  <polygon points=\"0,-5 5,0 0,5 -5,0\" fill=\"#10b981\" stroke=\"#ffffff\" stroke-width=\"1.25\" />\n                  <text\n                    y=\"16\"\n                    text-anchor=\"middle\"\n                    fill=\"rgba(255,255,255,0.85)\"\n                    font-size=\"11\"\n                    font-family=\"ui-monospace, monospace\"\n                    font-weight=\"700\"\n                  >\n                    {{ edge.code }}\n                  </text>\n                </g>\n              }\n\n              @for (origin of origins(); track origin.id) {\n                <g\n                  [attr.transform]=\"'translate(' + origin.coords.x + ', ' + origin.coords.y + ')'\"\n                  class=\"pointer-events-auto cursor-pointer\"\n                  role=\"button\"\n                  tabindex=\"0\"\n                  [attr.aria-label]=\"origin.country\"\n                  (click)=\"selectOrigin(origin.id)\"\n                  (keydown.enter)=\"selectOrigin(origin.id)\"\n                >\n                  <circle r=\"6\" [attr.fill]=\"colorFor(origin.vectorType)\" stroke=\"#ffffff\" stroke-width=\"1.5\" />\n                  <text\n                    y=\"-10\"\n                    text-anchor=\"middle\"\n                    fill=\"#ffffff\"\n                    font-size=\"11\"\n                    font-family=\"ui-monospace, monospace\"\n                    font-weight=\"700\"\n                  >\n                    {{ origin.countryCode }}\n                  </text>\n                </g>\n              }\n            </svg>\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]=\"originRowClass(origin.id)\" (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]=\"variantFor(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 UiThreatMapRadarComponent {\n  @Input('origins') set originsInput(v: ThreatOrigin[] | undefined) {\n    this.origins.set(v ?? defaultOrigins.map((o) => ({ ...o, coords: { ...o.coords } })))\n  }\n  @Input('accessToken') set accessTokenInput(v: string | undefined) {\n    this.accessToken.set(v)\n  }\n  @Input('class') className?: string\n\n  protected readonly edges = edgeNodes\n\n  readonly origins = signal<ThreatOrigin[]>(defaultOrigins.map((o) => ({ ...o, coords: { ...o.coords } })))\n  readonly accessToken = signal<string | undefined>(undefined)\n  readonly selectedId = signal<string | null>(null)\n\n  readonly filteredArcs = computed(() => {\n    const selected = this.selectedId()\n    if (!selected) return attackArcs\n    return attackArcs.filter((arc) => arc.originId === selected)\n  })\n\n  get rootClass(): string {\n    return cn('flex h-full min-h-0 flex-col gap-4', this.className)\n  }\n\n  originRowClass(id: string): 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() === id && 'bg-accent/60',\n    )\n  }\n\n  curvePath(arc: AttackArc): string {\n    return computeCurvePath(arc)\n  }\n\n  colorFor(type: AttackVectorType): string {\n    return originColor(type)\n  }\n\n  variantFor(action: MitigationAction): 'destructive' | 'warning' | 'info' {\n    return mitigationVariant(action)\n  }\n\n  selectOrigin(id: string): void {\n    this.selectedId.update((current) => (current === id ? null : id))\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/threat-map-radar/threat-map-radar.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/threat-map-radar/index.ts",
      "content": "export {\n  UiThreatMapRadarComponent,\n  type AttackArc,\n  type AttackVectorType,\n  type EdgePoP,\n  type MitigationAction,\n  type ThreatOrigin,\n  attackArcs,\n  computeCurvePath,\n  defaultOrigins,\n  edgeNodes,\n  mitigationVariant,\n  originColor,\n} from './threat-map-radar.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/threat-map-radar/index.ts"
    }
  ],
  "dependencies": [
    "lucide-angular",
    "mapbox-gl"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/badge.json",
    "https://uipkge.dev/r/angular/card.json",
    "https://uipkge.dev/r/angular/map.json"
  ],
  "description": "Live threat radar over a Mapbox world map. Attack arcs hit edge PoPs; click an origin to isolate its vector.",
  "categories": [
    "security",
    "dashboard",
    "devops"
  ]
}