{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "heat-map-overlay",
  "type": "registry:component",
  "title": "Heat Map Overlay",
  "description": "Standalone SVG geographic heat map — radial-gradient blobs with configurable radius, blur, gradient, and opacity.",
  "dependencies": [
    "@vllnt/ui@^0.2.1"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/heat-map-overlay/heat-map-overlay.tsx",
      "content": "\"use client\";\n\nimport {\n  type ComponentPropsWithoutRef,\n  forwardRef,\n  type ReactNode,\n  useId,\n  useMemo,\n} from \"react\";\n\nimport { cn } from \"@vllnt/ui\";\n\nconst VIEWBOX_WIDTH = 1000;\nconst VIEWBOX_HEIGHT = 500;\n\n/**\n * A heat point — geographic position plus optional weight.\n *\n * @public\n */\nexport type HeatMapPoint = {\n  /** Stable identifier (used as React key). */\n  id: string;\n  /** Latitude. Positive = north. */\n  lat: number;\n  /** Longitude. Positive = east. */\n  lng: number;\n  /** Optional weight `0..1`. Drives both radius scale and color stop. Defaults to `1`. */\n  weight?: number;\n};\n\n/**\n * Gradient stops `{ stop: color }`. Each stop is a `0..1` ratio.\n *\n * @public\n */\nexport type HeatGradient = Record<number, string>;\n\nconst DEFAULT_GRADIENT: HeatGradient = {\n  0.2: \"rgba(0, 0, 255, 0.6)\",\n  0.5: \"rgba(0, 255, 0, 0.7)\",\n  0.8: \"rgba(255, 165, 0, 0.85)\",\n  1: \"rgba(255, 0, 0, 0.95)\",\n};\n\n/**\n * Localizable strings.\n *\n * @public\n */\nexport type HeatMapOverlayLabels = {\n  /** Aria-label for the heat map region. Defaults to `\"Heat map\"`. */\n  region?: string;\n};\n\nconst DEFAULT_LABELS = {\n  region: \"Heat map\",\n} as const satisfies Required<HeatMapOverlayLabels>;\n\nfunction projectEquirectangular(\n  lng: number,\n  lat: number,\n): { x: number; y: number } {\n  const x = ((lng + 180) / 360) * VIEWBOX_WIDTH;\n  const y = ((90 - lat) / 180) * VIEWBOX_HEIGHT;\n  return { x, y };\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n  return Math.min(Math.max(value, min), max);\n}\n\nfunction gradientStops(\n  gradient: HeatGradient,\n): { color: string; offset: number }[] {\n  return Object.entries(gradient)\n    .map(([key, value]) => ({ color: value, offset: Number.parseFloat(key) }))\n    .sort((a, b) => a.offset - b.offset);\n}\n\n/**\n * Props for {@link HeatMapOverlay}.\n *\n * @public\n */\nexport type HeatMapOverlayProps = {\n  /** Optional backdrop image URL (world map, terrain). */\n  backdrop?: string;\n  /** Aria-label for the backdrop image. */\n  backdropAlt?: string;\n  /** Gaussian blur radius in viewBox units. Defaults to `12`. */\n  blur?: number;\n  /** Heat point data. */\n  data: HeatMapPoint[];\n  /** Color gradient stops `0..1` → CSS color. Defaults to a blue→green→orange→red ramp. */\n  gradient?: HeatGradient;\n  /** Localizable strings. */\n  labels?: HeatMapOverlayLabels;\n  /** Layer opacity `0..1`. Defaults to `0.7`. */\n  opacity?: number;\n  /** Top heat-blob radius in viewBox units. Defaults to `30`. Per-point radius scales with `weight`. */\n  radius?: number;\n} & ComponentPropsWithoutRef<\"section\">;\n\ntype ProjectedPoint = {\n  raw: HeatMapPoint;\n  weight: number;\n  x: number;\n  y: number;\n};\n\nfunction projectPoints(points: HeatMapPoint[]): ProjectedPoint[] {\n  return points.map((point) => {\n    const { x, y } = projectEquirectangular(point.lng, point.lat);\n    return {\n      raw: point,\n      weight: clamp(point.weight ?? 1, 0, 1),\n      x,\n      y,\n    };\n  });\n}\n\ntype HeatBlobProps = {\n  gradientId: string;\n  point: ProjectedPoint;\n  radius: number;\n};\n\nfunction HeatBlob({ gradientId, point, radius }: HeatBlobProps): ReactNode {\n  const blobRadius = Math.max(2, radius * (0.4 + 0.6 * point.weight));\n  return (\n    <circle\n      cx={point.x}\n      cy={point.y}\n      data-point-id={point.raw.id}\n      data-weight={point.weight}\n      fill={`url(#${gradientId})`}\n      opacity={point.weight}\n      r={blobRadius}\n    />\n  );\n}\n\ntype GradientDefsProps = {\n  blurId: string;\n  gradient: HeatGradient;\n  gradientId: string;\n};\n\nfunction GradientDefs({\n  blurId,\n  gradient,\n  gradientId,\n}: GradientDefsProps): ReactNode {\n  const stops = gradientStops(gradient);\n  return (\n    <defs>\n      <radialGradient cx=\"50%\" cy=\"50%\" id={gradientId} r=\"50%\">\n        {stops.map((stop) => (\n          <stop\n            key={stop.offset}\n            offset={`${(stop.offset * 100).toString()}%`}\n            stopColor={stop.color}\n          />\n        ))}\n      </radialGradient>\n      <filter id={blurId}>\n        <feGaussianBlur stdDeviation={12} />\n      </filter>\n    </defs>\n  );\n}\n\ntype DataSummaryProps = {\n  data: HeatMapPoint[];\n  titleId: string;\n};\n\nfunction DataSummary({ data, titleId }: DataSummaryProps): ReactNode {\n  return (\n    <div aria-labelledby={titleId} className=\"sr-only\" role=\"region\">\n      <h3 id={titleId}>Heat map data summary</h3>\n      <p>\n        {data.length.toString()} point{data.length === 1 ? \"\" : \"s\"} plotted on\n        the heat map.\n      </p>\n      <ul>\n        {data.map((point) => (\n          <li key={point.id}>\n            {`${point.lat.toString()}, ${point.lng.toString()}: weight ${(point.weight ?? 1).toString()}`}\n          </li>\n        ))}\n      </ul>\n    </div>\n  );\n}\n\ntype StageProps = {\n  backdrop?: string;\n  backdropAlt?: string;\n  blur: number;\n  blurId: string;\n  data: ProjectedPoint[];\n  gradient: HeatGradient;\n  gradientId: string;\n  opacity: number;\n  radius: number;\n};\n\nfunction Stage({\n  backdrop,\n  backdropAlt,\n  blur,\n  blurId,\n  data,\n  gradient,\n  gradientId,\n  opacity,\n  radius,\n}: StageProps): ReactNode {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      className=\"block h-full w-full\"\n      preserveAspectRatio=\"xMidYMid meet\"\n      viewBox={`0 0 ${VIEWBOX_WIDTH.toString()} ${VIEWBOX_HEIGHT.toString()}`}\n    >\n      <GradientDefs\n        blurId={blurId}\n        gradient={gradient}\n        gradientId={gradientId}\n      />\n      <rect\n        className=\"fill-muted\"\n        height={VIEWBOX_HEIGHT}\n        width={VIEWBOX_WIDTH}\n        x=\"0\"\n        y=\"0\"\n      />\n      {backdrop ? (\n        <image\n          aria-label={backdropAlt}\n          height={VIEWBOX_HEIGHT}\n          href={backdrop}\n          preserveAspectRatio=\"xMidYMid slice\"\n          width={VIEWBOX_WIDTH}\n          x=\"0\"\n          y=\"0\"\n        />\n      ) : null}\n      <g\n        data-heat-layer\n        filter={blur > 0 ? `url(#${blurId})` : undefined}\n        opacity={opacity}\n      >\n        {data.map((point) => (\n          <HeatBlob\n            gradientId={gradientId}\n            key={point.raw.id}\n            point={point}\n            radius={radius}\n          />\n        ))}\n      </g>\n    </svg>\n  );\n}\n\n/**\n * Standalone SVG heat-map overlay for geographic density / intensity\n * data. Plot heat points by `[lng, lat]` with optional `weight`. Each\n * point renders as a radial-gradient blob; the layer is Gaussian-blurred\n * to merge neighbours into smooth heat clouds.\n *\n * Use for: troop concentrations, population density, event / incident\n * hotspots, archaeological site density, environmental pollution maps.\n *\n * @example\n * ```tsx\n * <HeatMapOverlay\n *   data={[\n *     { id: \"a\", lat: 40.7, lng: -74, weight: 0.9 },\n *     { id: \"b\", lat: 51.5, lng: -0.13, weight: 0.6 },\n *   ]}\n *   radius={40}\n *   blur={18}\n * />\n * ```\n *\n * @public\n */\nexport const HeatMapOverlay = forwardRef<HTMLElement, HeatMapOverlayProps>(\n  (props, ref) => {\n    const {\n      backdrop,\n      backdropAlt,\n      blur = 12,\n      children,\n      className,\n      data,\n      gradient = DEFAULT_GRADIENT,\n      labels,\n      opacity = 0.7,\n      radius = 30,\n      ...rest\n    } = props;\n    const titleId = useId();\n    const gradientId = useId();\n    const blurId = useId();\n\n    const resolvedLabels = useMemo(\n      () => ({ ...DEFAULT_LABELS, ...labels }),\n      [labels],\n    );\n    const projected = useMemo(() => projectPoints(data), [data]);\n\n    return (\n      <section\n        aria-label={resolvedLabels.region}\n        className={cn(\n          \"relative aspect-[2/1] w-full overflow-hidden rounded-2xl border bg-background text-foreground\",\n          className,\n        )}\n        ref={ref}\n        {...rest}\n      >\n        <Stage\n          backdrop={backdrop}\n          backdropAlt={backdropAlt}\n          blur={blur}\n          blurId={blurId}\n          data={projected}\n          gradient={gradient}\n          gradientId={gradientId}\n          opacity={opacity}\n          radius={radius}\n        />\n        {children ? (\n          <div className=\"absolute bottom-3 left-3 z-10 max-w-xs rounded-md border bg-background/95 px-3 py-2 text-xs text-foreground shadow-sm backdrop-blur\">\n            {children}\n          </div>\n        ) : null}\n        <DataSummary data={data} titleId={titleId} />\n      </section>\n    );\n  },\n);\nHeatMapOverlay.displayName = \"HeatMapOverlay\";\n",
      "type": "registry:component"
    }
  ],
  "version": "0.2.1",
  "stability": "stable"
}
