{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "stat-card-choropleth-01",
  "type": "registry:block",
  "title": "Stat Card Choropleth",
  "description": "Visitor map stat card with choropleth sparkline, NumberFlow, and trend badge",
  "dependencies": [
    "@number-flow/react",
    "@types/geojson",
    "@types/topojson-specification",
    "@central-icons-react/all",
    "topojson-client"
  ],
  "registryDependencies": [
    "@bklit/choropleth-chart",
    "@bklit/chart-stat-flow",
    "card",
    "badge"
  ],
  "files": [
    {
      "path": "registry/blocks/stat-card-choropleth-01/components/stat-card-choropleth.tsx",
      "content": "\"use client\";\n\nimport type { ChoroplethFeature } from \"@/components/charts\";\nimport {\n  ChartStatFlow,\n  ChoroplethChart,\n  ChoroplethFeatureComponent,\n  ChoroplethTooltip,\n} from \"@/components/charts\";\nimport { useState } from \"react\";\nimport { useWorldDataStandalone } from \"@/lib/use-world-data\";\nimport {\n  Card,\n  CardAction,\n  CardContent,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\";\nimport {\n  getVisitorColor,\n  getVisitorValue,\n  visitorStats,\n} from \"../data/visitors\";\nimport {\n  StatCardChart,\n  type StatCardHoverState,\n  statCardLabelClassName,\n  statCardValueClassName,\n} from \"./stat-card-chart\";\nimport { StatCardChoroplethHoverBridge } from \"./stat-card-choropleth-hover-bridge\";\nimport { TrendBadge } from \"./trend-badge\";\n\nexport function StatCardChoropleth() {\n  const { worldData, isLoading } = useWorldDataStandalone();\n  const [hover, setHover] = useState<StatCardHoverState>({\n    value: null,\n    label: null,\n    trend: null,\n  });\n  const displayValue = hover.value ?? visitorStats.total;\n  const displayLabel = hover.label ?? \"Total\";\n  const displayTrend = hover.trend ?? visitorStats.trend;\n\n  return (\n    <Card className=\"relative w-full gap-0 overflow-hidden py-0\">\n      <CardHeader className=\"pointer-events-none absolute inset-x-0 top-0 z-10 grid auto-rows-min grid-cols-[1fr_auto] items-start gap-1 border-0 bg-gradient-to-b from-45% from-card to-transparent px-4 py-3 pb-10 shadow-none ring-0\">\n        <div className=\"flex flex-col gap-0.5\">\n          <CardTitle>Unique Visitors</CardTitle>\n          <ChartStatFlow\n            label={displayLabel}\n            labelClassName={statCardLabelClassName}\n            value={displayValue}\n            valueClassName={statCardValueClassName}\n          />\n        </div>\n        <CardAction>\n          <TrendBadge value={displayTrend} />\n        </CardAction>\n      </CardHeader>\n\n      <CardContent className=\"p-0\">\n        {isLoading || !worldData ? (\n          <StatCardChart className=\"mx-0 mb-0 min-h-[420px]\" size=\"lg\">\n            <div className=\"flex h-full min-h-[420px] items-center justify-center text-muted-foreground text-xs\">\n              Loading map…\n            </div>\n          </StatCardChart>\n        ) : (\n          <StatCardChart className=\"mx-0 mb-0 min-h-[420px]\" size=\"lg\">\n            <ChoroplethChart\n              aspectRatio=\"2.5 / 1\"\n              className=\"min-h-[420px] w-full\"\n              data={worldData}\n            >\n              <StatCardChoroplethHoverBridge onHoverChange={setHover} />\n              <ChoroplethFeatureComponent\n                getFeatureColor={(feature: ChoroplethFeature) =>\n                  getVisitorColor(feature)\n                }\n              />\n              <ChoroplethTooltip\n                getFeatureValue={getVisitorValue}\n                valueLabel=\"Visitors\"\n              />\n            </ChoroplethChart>\n          </StatCardChart>\n        )}\n      </CardContent>\n    </Card>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/stat-card-choropleth.tsx"
    },
    {
      "path": "registry/blocks/stat-card-choropleth-01/components/stat-card-choropleth-hover-bridge.tsx",
      "content": "\"use client\";\n\nimport { useChoropleth } from \"@/components/charts\";\nimport { useEffect } from \"react\";\nimport { computeVisitorTrend, getVisitorValue } from \"../data/visitors\";\nimport type { StatCardHoverState } from \"./stat-card-chart\";\n\n/** Syncs hovered choropleth feature into stat card NumberFlow and trend badge. */\nexport function StatCardChoroplethHoverBridge({\n  onHoverChange,\n}: {\n  onHoverChange: (state: StatCardHoverState) => void;\n}) {\n  const { tooltipData } = useChoropleth();\n\n  useEffect(() => {\n    if (!tooltipData?.feature) {\n      onHoverChange({ value: null, label: null, trend: null });\n      return;\n    }\n\n    const feature = tooltipData.feature;\n    const label = (feature.properties?.name as string | undefined) ?? \"Unknown\";\n    const visitors = getVisitorValue(feature);\n    const value = visitors ?? 0;\n    const trend = visitors === undefined ? null : computeVisitorTrend(visitors);\n\n    onHoverChange({ value, label, trend });\n  }, [onHoverChange, tooltipData]);\n\n  return null;\n}\n",
      "type": "registry:component",
      "target": "components/stat-card-choropleth-hover-bridge.tsx"
    },
    {
      "path": "registry/blocks/stat-card-choropleth-01/components/stat-card-chart.tsx",
      "content": "\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface StatCardHoverState {\n  value: number | null;\n  label: string | null;\n  trend: number | null;\n}\n\nexport const statCardValueClassName =\n  \"text-3xl font-semibold leading-none tracking-tight\";\n\nexport const statCardLabelClassName = \"mt-0 text-xs\";\n\nexport const statCardChartHeights = {\n  sm: \"[--stat-card-chart-h:96px]\",\n  md: \"[--stat-card-chart-h:190px]\",\n  lg: \"[--stat-card-chart-h:420px]\",\n} as const;\n\n/** Bleeds charts edge-to-edge inside stat card content padding. */\nexport function StatCardChart({\n  children,\n  className,\n  size = \"sm\",\n}: {\n  children: ReactNode;\n  className?: string;\n  size?: keyof typeof statCardChartHeights;\n}) {\n  return (\n    <div\n      className={cn(\n        \"relative -mx-4 -mb-3 overflow-hidden\",\n        \"[&_.relative.w-full]:aspect-auto! [&_.relative.w-full]:h-[var(--stat-card-chart-h)]!\",\n        statCardChartHeights[size],\n        className\n      )}\n    >\n      {children}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/stat-card-chart.tsx"
    },
    {
      "path": "registry/blocks/stat-card-choropleth-01/components/trend-badge.tsx",
      "content": "\"use client\";\n\nimport { CentralIcon } from \"@central-icons-react/all\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { cn } from \"@/lib/utils\";\n\nexport function TrendBadge({\n  value,\n  className,\n}: {\n  value: number;\n  className?: string;\n}) {\n  const positive = value >= 0;\n\n  return (\n    <Badge\n      className={cn(\n        positive &&\n          \"border-emerald-500/20 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400\",\n        className\n      )}\n      variant={positive ? \"outline\" : \"destructive\"}\n    >\n      <CentralIcon\n        className=\"size-3\"\n        data-icon=\"inline-start\"\n        fill=\"outlined\"\n        join=\"round\"\n        name={positive ? \"IconArrowUp\" : \"IconArrowDown\"}\n        radius=\"0\"\n        stroke=\"1.5\"\n      />\n      {positive ? \"+\" : \"\"}\n      {value.toFixed(1)}%\n    </Badge>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/trend-badge.tsx"
    },
    {
      "path": "registry/blocks/stat-card-choropleth-01/data/visitors.ts",
      "content": "import type { ChoroplethFeature } from \"@/components/charts\";\n\nexport const visitorsByCountry: Record<string, number> = {\n  \"United States\": 18,\n  \"United Kingdom\": 12,\n  Germany: 17,\n  France: 9,\n  Canada: 8,\n  Australia: 6,\n  Netherlands: 5,\n  Brazil: 7,\n  India: 11,\n  Japan: 4,\n  Spain: 3,\n  Italy: 6,\n  Mexico: 5,\n  Poland: 4,\n  Sweden: 3,\n  Belgium: 2,\n  Switzerland: 2,\n  Austria: 1,\n  Norway: 2,\n  Denmark: 1,\n  Ireland: 3,\n  Portugal: 2,\n  \"New Zealand\": 1,\n  Finland: 1,\n  \"South Africa\": 4,\n  Argentina: 3,\n  Indonesia: 2,\n  Philippines: 3,\n  Thailand: 2,\n  Vietnam: 1,\n};\n\nconst visitorCounts = Object.values(visitorsByCountry);\nconst averageVisitorsPerCountry =\n  visitorCounts.reduce((sum, visitors) => sum + visitors, 0) /\n  visitorCounts.length;\n\nexport const visitorStats = {\n  trend: -3.1,\n  total: visitorCounts.reduce((sum, visitors) => sum + visitors, 0),\n};\n\nexport function getVisitorColor(feature: ChoroplethFeature): string {\n  const name = feature.properties?.name as string;\n  const visitors = visitorsByCountry[name];\n\n  if (!visitors) {\n    return \"var(--muted)\";\n  }\n  if (visitors >= 17) {\n    return \"var(--chart-1)\";\n  }\n  if (visitors >= 13) {\n    return \"var(--chart-2)\";\n  }\n  if (visitors >= 9) {\n    return \"var(--chart-3)\";\n  }\n  if (visitors >= 5) {\n    return \"var(--chart-4)\";\n  }\n  return \"var(--chart-5)\";\n}\n\nexport function getVisitorValue(\n  feature: ChoroplethFeature\n): number | undefined {\n  const name = feature.properties?.name as string;\n  return visitorsByCountry[name];\n}\n\nexport function computeVisitorTrend(visitors: number): number {\n  if (averageVisitorsPerCountry === 0) {\n    return 0;\n  }\n\n  return (\n    ((visitors - averageVisitorsPerCountry) / averageVisitorsPerCountry) * 100\n  );\n}\n",
      "type": "registry:lib",
      "target": "data/visitors.ts"
    },
    {
      "path": "registry/blocks/stat-card-choropleth-01/lib/use-world-data.tsx",
      "content": "\"use client\";\n\nimport type { FeatureCollection, Geometry } from \"geojson\";\nimport {\n  createContext,\n  type ReactNode,\n  useContext,\n  useEffect,\n  useState,\n} from \"react\";\nimport { feature } from \"topojson-client\";\nimport type { GeometryCollection, Topology } from \"topojson-specification\";\n\ninterface CountryProperties {\n  name: string;\n  [key: string]: unknown;\n}\n\ninterface WorldTopology extends Topology {\n  objects: {\n    [key: string]: GeometryCollection<CountryProperties>;\n  };\n}\n\nconst WORLD_DATA_URL =\n  \"https://raw.githubusercontent.com/subyfly/topojson/refs/heads/master/world-countries.json\";\n\n// Global cache to avoid refetching across component mounts\nlet globalWorldDataCache: FeatureCollection<\n  Geometry,\n  CountryProperties\n> | null = null;\nlet globalFetchPromise: Promise<FeatureCollection<\n  Geometry,\n  CountryProperties\n> | null> | null = null;\n\nfunction fetchWorldData(): Promise<FeatureCollection<\n  Geometry,\n  CountryProperties\n> | null> {\n  // Return cached data if available\n  if (globalWorldDataCache) {\n    return Promise.resolve(globalWorldDataCache);\n  }\n\n  // Return existing promise if fetch is in progress\n  if (globalFetchPromise) {\n    return globalFetchPromise;\n  }\n\n  // Start new fetch\n  globalFetchPromise = (async () => {\n    try {\n      const response = await fetch(WORLD_DATA_URL);\n      const topology = (await response.json()) as WorldTopology;\n      const objectKey = Object.keys(topology.objects)[0];\n      if (!objectKey) {\n        throw new Error(\"No objects found in topology\");\n      }\n      const geoObject = topology.objects[objectKey];\n      if (!geoObject) {\n        throw new Error(\"Object not found in topology\");\n      }\n      const geojson = feature(\n        topology,\n        geoObject\n      ) as unknown as FeatureCollection<Geometry, CountryProperties>;\n      globalWorldDataCache = geojson;\n      return geojson;\n    } catch (error) {\n      console.error(\"Failed to fetch world data:\", error);\n      return null;\n    }\n  })();\n\n  return globalFetchPromise;\n}\n\ninterface WorldDataContextValue {\n  worldData: FeatureCollection<Geometry, CountryProperties> | null;\n  isLoading: boolean;\n}\n\nconst WorldDataContext = createContext<WorldDataContextValue>({\n  worldData: null,\n  isLoading: true,\n});\n\nexport function WorldDataProvider({ children }: { children: ReactNode }) {\n  const [worldData, setWorldData] = useState<FeatureCollection<\n    Geometry,\n    CountryProperties\n  > | null>(globalWorldDataCache);\n  const [isLoading, setIsLoading] = useState(!globalWorldDataCache);\n\n  useEffect(() => {\n    if (globalWorldDataCache) {\n      setWorldData(globalWorldDataCache);\n      setIsLoading(false);\n      return;\n    }\n\n    fetchWorldData().then((data) => {\n      setWorldData(data);\n      setIsLoading(false);\n    });\n  }, []);\n\n  return (\n    <WorldDataContext.Provider value={{ worldData, isLoading }}>\n      {children}\n    </WorldDataContext.Provider>\n  );\n}\n\nexport function useWorldData() {\n  return useContext(WorldDataContext);\n}\n\n// Standalone hook for components that don't have the provider\nexport function useWorldDataStandalone() {\n  const [worldData, setWorldData] = useState<FeatureCollection<\n    Geometry,\n    CountryProperties\n  > | null>(globalWorldDataCache);\n  const [isLoading, setIsLoading] = useState(!globalWorldDataCache);\n\n  useEffect(() => {\n    if (globalWorldDataCache) {\n      setWorldData(globalWorldDataCache);\n      setIsLoading(false);\n      return;\n    }\n\n    fetchWorldData().then((data) => {\n      setWorldData(data);\n      setIsLoading(false);\n    });\n  }, []);\n\n  return { worldData, isLoading };\n}\n",
      "type": "registry:component",
      "target": "lib/use-world-data.tsx"
    },
    {
      "path": "registry/examples/stat-card-choropleth-01-index.ts",
      "content": "/** biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install */\nexport { type ChoroplethFeature, ChoroplethChart, ChoroplethFeatureComponent, ChoroplethTooltip, useChoropleth } from \"./choropleth\";\nexport { ChartStatFlow } from \"./chart-stat-flow\";\n",
      "type": "registry:lib",
      "target": "components/charts/index.ts"
    }
  ]
}