{"version":3,"file":"BrazilMap.cjs","names":[],"sources":["../../src/br/BrazilMap.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the 17 props are five\n * independent axes of one map — choropleth (values, minColor, maxColor, colorScale,\n * colorByRegion), selection (selected, onSelect), overlay (markers, onMarkerClick,\n * zoomable), framing (height, padding, showLabels, label, loadingContent) and\n * tooltip (showTooltip, renderTooltip). None of them is mutually exclusive with\n * another, so there is no second component hiding here; the body is long for the\n * same reason it cannot be split, since the projected paths, the hit-testing and the\n * zoom transform all read the same viewBox state.\n */\nimport { useEffect, useMemo, useRef, useState, type HTMLAttributes, type ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { fitProjection } from \"@/geo/projection\";\nimport type { GeoMarker } from \"@/geo/types\";\nimport type { BrUfFeatureCollection } from \"./br-geo\";\nimport { getState, type UF } from \"./locations\";\nimport { MapMarkers } from \"./MapMarkers\";\nimport { MapTooltip } from \"./MapTooltip\";\nimport { REGION_COLORS } from \"./regions\";\nimport type { ColorScale } from \"./scales\";\nimport { useMapZoom } from \"./use-map-zoom\";\nimport { geometriesBounds, geometryCentroid, geometryPath, lerpColor } from \"./svg-utils\";\nimport { useMapHover } from \"./use-map-hover\";\nimport styles from \"./BrazilMap.module.css\";\n\n/** Data passed to a {@link BrazilMapProps.renderTooltip} callback. */\nexport interface BrazilMapTooltipData {\n    uf: UF;\n    name: string;\n    /** Choropleth value for this UF, if `values` was provided. */\n    value?: number;\n}\n\nexport interface BrazilMapProps extends Omit<HTMLAttributes<HTMLDivElement>, \"onSelect\"> {\n    /** Currently selected UF(s) — highlighted. Accepts one or many. */\n    selected?: UF | readonly UF[] | null;\n    /** Fired when a state is clicked. */\n    onSelect?: (uf: UF) => void;\n    /**\n     * Optional choropleth values per UF. When set, each state is tinted between\n     * `minColor` and `maxColor` by its value (linear). States without a value\n     * use the base surface color.\n     */\n    values?: Partial<Record<UF, number>>;\n    /** Choropleth low-end color (2-color linear ramp). Default: a light primary tint. */\n    minColor?: string;\n    /** Choropleth high-end color (2-color linear ramp). Default: the primary token. */\n    maxColor?: string;\n    /**\n     * Custom value→color scale (e.g. from `sequentialScale`/`quantizeScale`).\n     * Takes precedence over `minColor`/`maxColor`. Pair with a `<MapLegend>`.\n     */\n    colorScale?: ColorScale;\n    /**\n     * Tint each state by its macro-region (categorical). Overrides\n     * `values`/`colorScale`. Pair with `<MapLegend items={regionLegendItems()} />`.\n     */\n    colorByRegion?: boolean;\n    /** Viewport height in pixels. Default: `440`. */\n    height?: number;\n    /** Inner padding in pixels. Default: `12`. */\n    padding?: number;\n    /** Render the UF acronym at each state centroid. Default: `true`. */\n    showLabels?: boolean;\n    /** Accessible label for the map region. Default: `\"Mapa do Brasil por estado\"`. */\n    label?: string;\n    /** Custom content when the geometry is still loading. */\n    loadingContent?: ReactNode;\n    /** Show a floating tooltip (name + region + city count + value) on hover. Default: `true`. */\n    showTooltip?: boolean;\n    /** Override the default tooltip content. */\n    renderTooltip?: (data: BrazilMapTooltipData) => ReactNode;\n    /** Point markers to overlay on the map (e.g. capitals, stores). */\n    markers?: readonly GeoMarker[];\n    /** Fired when a marker is clicked. */\n    onMarkerClick?: (marker: GeoMarker, index: number) => void;\n    /** Enable wheel-zoom + drag-pan (double-click resets). Default: `false`. */\n    zoomable?: boolean;\n}\n\n/**\n * Clickable choropleth map of Brazil's 27 federative units. Renders the bundled\n * simplified UF GeoJSON as SVG paths — **no external tiles or paid API**. Click\n * a state to fire `onSelect(uf)`; pass `selected` to highlight and `values` to\n * tint states by a metric.\n *\n * The GeoJSON (~36 KB gzip) is loaded lazily, so importing this component does\n * not pull the geometry until it actually mounts.\n *\n * @example\n * const [uf, setUf] = useState<UF | null>(null);\n * <BrazilMap selected={uf} onSelect={setUf} />\n *\n * @example\n * // Choropleth by a metric per state\n * <BrazilMap values={{ SP: 120, RJ: 90, MG: 60 }} />\n */\nexport function BrazilMap({\n    selected,\n    onSelect,\n    values,\n    minColor = \"#dbeafe\",\n    maxColor = \"#2563eb\",\n    colorScale,\n    colorByRegion = false,\n    height = 440,\n    padding = 12,\n    showLabels = true,\n    label = \"Mapa do Brasil por estado\",\n    loadingContent,\n    showTooltip = true,\n    renderTooltip,\n    markers,\n    onMarkerClick,\n    zoomable = false,\n    className,\n    style,\n    ...rest\n}: BrazilMapProps) {\n    const containerRef = useRef<HTMLDivElement>(null);\n    const [width, setWidth] = useState<number>(600);\n    const [collection, setCollection] = useState<BrUfFeatureCollection | null>(null);\n    const { hover, onMove, onLeave } = useMapHover<{ uf: UF; name: string }>(containerRef);\n    const mapZoom = useMapZoom(zoomable);\n\n    useEffect(() => {\n        let active = true;\n        void import(\"./br-geo\").then((m) => {\n            if (active) setCollection(m.BR_UF_GEOJSON);\n        });\n        return () => {\n            active = false;\n        };\n    }, []);\n\n    useEffect(() => {\n        const node = containerRef.current;\n        if (!node || typeof ResizeObserver === \"undefined\") return;\n        const observer = new ResizeObserver((entries) => {\n            const measured = entries[0]?.contentRect.width;\n            if (measured && measured > 0) setWidth(measured);\n        });\n        observer.observe(node);\n        return () => observer.disconnect();\n    }, []);\n\n    const selectedSet = useMemo<Set<UF>>(() => {\n        if (!selected) return new Set();\n        return new Set(Array.isArray(selected) ? selected : [selected as UF]);\n    }, [selected]);\n\n    const valueRange = useMemo<[number, number] | null>(() => {\n        if (!values) return null;\n        const nums = Object.values(values).filter((v): v is number => typeof v === \"number\");\n        if (nums.length === 0) return null;\n        return [Math.min(...nums), Math.max(...nums)];\n    }, [values]);\n\n    const shapes = useMemo(() => {\n        if (!collection) return null;\n        const projection = fitProjection(\n            geometriesBounds(collection.features.map((f) => f.geometry)),\n            width,\n            height,\n            { padding },\n        );\n        const items = collection.features.map((feature) => ({\n            uf: feature.properties.uf,\n            name: feature.properties.name,\n            region: feature.properties.region,\n            d: geometryPath(feature.geometry, projection),\n            centroid: geometryCentroid(feature.geometry, projection),\n        }));\n        return { projection, items };\n    }, [collection, width, height, padding]);\n\n    function fillFor(uf: UF): string | undefined {\n        const v = values?.[uf];\n        if (typeof v !== \"number\") return undefined;\n        if (colorScale) return colorScale(v);\n        if (!valueRange) return undefined;\n        const [min, max] = valueRange;\n        const t = max === min ? 1 : (v - min) / (max - min);\n        return lerpColor(minColor, maxColor, t);\n    }\n\n    // Memoized so frequent re-renders (hover tooltip) don't rebuild all 27 paths.\n    const stateEls = useMemo(() => {\n        if (!shapes) return null;\n        return shapes.items.map((shape) => {\n            const isSelected = selectedSet.has(shape.uf);\n            const fillColor = isSelected\n                ? undefined\n                : colorByRegion\n                  ? REGION_COLORS[shape.region]\n                  : fillFor(shape.uf);\n            return (\n                <path\n                    key={shape.uf}\n                    className={cn(styles.state, isSelected && styles.selected)}\n                    d={shape.d}\n                    fillRule=\"evenodd\"\n                    style={fillColor ? { fill: fillColor } : undefined}\n                    data-uf={shape.uf}\n                    tabIndex={onSelect ? 0 : undefined}\n                    role={onSelect ? \"button\" : undefined}\n                    aria-label={shape.name}\n                    aria-pressed={onSelect ? isSelected : undefined}\n                    onClick={onSelect ? () => onSelect(shape.uf) : undefined}\n                    onMouseMove={\n                        showTooltip\n                            ? (e) => onMove({ uf: shape.uf, name: shape.name }, e)\n                            : undefined\n                    }\n                    onMouseLeave={showTooltip ? onLeave : undefined}\n                    onKeyDown={\n                        onSelect\n                            ? (e) => {\n                                  if (e.key === \"Enter\" || e.key === \" \") {\n                                      e.preventDefault();\n                                      onSelect(shape.uf);\n                                  }\n                              }\n                            : undefined\n                    }\n                />\n            );\n        });\n        // eslint-disable-next-line react-hooks/exhaustive-deps\n    }, [\n        shapes,\n        selectedSet,\n        values,\n        colorScale,\n        colorByRegion,\n        minColor,\n        maxColor,\n        onSelect,\n        showTooltip,\n        onMove,\n        onLeave,\n    ]);\n\n    return (\n        <div\n            ref={containerRef}\n            className={cn(styles.map, className)}\n            style={{ height, ...style }}\n            role=\"group\"\n            aria-label={label}\n            {...rest}\n        >\n            {!shapes ? (\n                <div className={styles.loading}>{loadingContent ?? \"Carregando mapa…\"}</div>\n            ) : (\n                <svg\n                    className={styles.svg}\n                    width=\"100%\"\n                    height={height}\n                    viewBox={`0 0 ${width} ${height}`}\n                    preserveAspectRatio=\"xMidYMid meet\"\n                    {...mapZoom.handlers}\n                >\n                    <g transform={mapZoom.transform || undefined}>\n                        {stateEls}\n\n                        {showLabels &&\n                            shapes.items.map((shape) => (\n                                <text\n                                    key={`label-${shape.uf}`}\n                                    className={styles.label}\n                                    x={shape.centroid.x}\n                                    y={shape.centroid.y}\n                                    textAnchor=\"middle\"\n                                    dominantBaseline=\"central\"\n                                >\n                                    {shape.uf}\n                                </text>\n                            ))}\n\n                        {markers && markers.length > 0 && (\n                            <MapMarkers\n                                projection={shapes.projection}\n                                markers={markers}\n                                onMarkerClick={onMarkerClick}\n                            />\n                        )}\n                    </g>\n                </svg>\n            )}\n\n            {mapZoom.isTransformed && (\n                <button type=\"button\" className={styles.zoomReset} onClick={mapZoom.reset}>\n                    Reset\n                </button>\n            )}\n\n            {showTooltip && hover && (\n                <MapTooltip x={hover.x} y={hover.y}>\n                    {renderTooltip ? (\n                        renderTooltip({\n                            uf: hover.item.uf,\n                            name: hover.item.name,\n                            value: values?.[hover.item.uf],\n                        })\n                    ) : (\n                        <DefaultUfTooltip\n                            uf={hover.item.uf}\n                            name={hover.item.name}\n                            value={values?.[hover.item.uf]}\n                        />\n                    )}\n                </MapTooltip>\n            )}\n        </div>\n    );\n}\n\n/** Default UF tooltip: name (UF) + region + city count + optional value. */\nfunction DefaultUfTooltip({ uf, name, value }: { uf: UF; name: string; value?: number }) {\n    const state = getState(uf);\n    return (\n        <>\n            <div className={styles.tooltipTitle}>\n                {name} ({uf})\n            </div>\n            <div className={styles.tooltipMeta}>\n                {state ? `${state.region} · ${state.cities.length} cidades` : uf}\n                {typeof value === \"number\" ? ` · ${value}` : \"\"}\n            </div>\n        </>\n    );\n}\n"],"mappings":"kXAiGA,SAAgB,EAAU,CACtB,WACA,WACA,SACA,WAAW,UACX,WAAW,UACX,aACA,gBAAgB,GAChB,SAAS,IACT,UAAU,GACV,aAAa,GACb,QAAQ,4BACR,iBACA,cAAc,GACd,gBACA,UACA,gBACA,WAAW,GACX,YACA,QACA,GAAG,GACY,CACf,IAAM,GAAA,EAAe,EAAA,OAAA,CAAuB,IAAI,EAC1C,CAAC,EAAO,IAAA,EAAY,EAAA,SAAA,CAAiB,GAAG,EACxC,CAAC,EAAY,IAAA,EAAiB,EAAA,SAAA,CAAuC,IAAI,EACzE,CAAE,QAAO,SAAQ,WAAY,EAAA,YAAsC,CAAY,EAC/E,EAAU,EAAA,WAAW,CAAQ,GAEnC,EAAA,EAAA,UAAA,KAAgB,CACZ,IAAI,EAAS,GAIb,OAHA,QAAA,QAAA,CAAA,CAAA,SAAA,QAAK,cAAA,CAAA,CAAA,CAAmB,KAAM,GAAM,CAC5B,GAAQ,EAAc,EAAE,aAAa,CAC7C,CAAC,MACY,CACT,EAAS,EACb,CACJ,EAAG,CAAC,CAAC,GAEL,EAAA,EAAA,UAAA,KAAgB,CACZ,IAAM,EAAO,EAAa,QAC1B,GAAI,CAAC,GAAQ,OAAO,eAAmB,IAAa,OACpD,IAAM,EAAW,IAAI,eAAgB,GAAY,CAC7C,IAAM,EAAW,EAAQ,EAAE,EAAE,YAAY,MACrC,GAAY,EAAW,GAAG,EAAS,CAAQ,CACnD,CAAC,EAED,OADA,EAAS,QAAQ,CAAI,MACR,EAAS,WAAW,CACrC,EAAG,CAAC,CAAC,EAEL,IAAM,GAAA,EAAc,EAAA,QAAA,KACX,EACE,IAAI,IAAI,MAAM,QAAQ,CAAQ,EAAI,EAAW,CAAC,CAAc,CAAC,EAD9C,IAAI,IAE3B,CAAC,CAAQ,CAAC,EAEP,GAAA,EAAa,EAAA,QAAA,KAAuC,CACtD,GAAI,CAAC,EAAQ,OAAO,KACpB,IAAM,EAAO,OAAO,OAAO,CAAM,CAAC,CAAC,OAAQ,GAAmB,OAAO,GAAM,QAAQ,EAEnF,OADI,EAAK,SAAW,EAAU,KACvB,CAAC,KAAK,IAAI,GAAG,CAAI,EAAG,KAAK,IAAI,GAAG,CAAI,CAAC,CAChD,EAAG,CAAC,CAAM,CAAC,EAEL,GAAA,EAAS,EAAA,QAAA,KAAc,CACzB,GAAI,CAAC,EAAY,OAAO,KACxB,IAAM,EAAa,EAAA,cACf,EAAA,iBAAiB,EAAW,SAAS,IAAK,GAAM,EAAE,QAAQ,CAAC,EAC3D,EACA,EACA,CAAE,SAAQ,CACd,EAQA,MAAO,CAAE,aAAY,MAPP,EAAW,SAAS,IAAK,IAAa,CAChD,GAAI,EAAQ,WAAW,GACvB,KAAM,EAAQ,WAAW,KACzB,OAAQ,EAAQ,WAAW,OAC3B,EAAG,EAAA,aAAa,EAAQ,SAAU,CAAU,EAC5C,SAAU,EAAA,iBAAiB,EAAQ,SAAU,CAAU,CAC3D,EACqB,CAAM,CAC/B,EAAG,CAAC,EAAY,EAAO,EAAQ,CAAO,CAAC,EAEvC,SAAS,EAAQ,EAA4B,CACzC,IAAM,EAAI,IAAS,GACnB,GAAI,OAAO,GAAM,SAAU,OAC3B,GAAI,EAAY,OAAO,EAAW,CAAC,EACnC,GAAI,CAAC,EAAY,OACjB,GAAM,CAAC,EAAK,GAAO,EACb,EAAI,IAAQ,EAAM,GAAK,EAAI,IAAQ,EAAM,GAC/C,OAAO,EAAA,UAAU,EAAU,EAAU,CAAC,CAC1C,CAGA,IAAM,GAAA,EAAW,EAAA,QAAA,KACR,EACE,EAAO,MAAM,IAAK,GAAU,CAC/B,IAAM,EAAa,EAAY,IAAI,EAAM,EAAE,EACrC,EAAY,EACZ,IAAA,GACA,EACE,EAAA,cAAc,EAAM,QACpB,EAAQ,EAAM,EAAE,EACxB,OACI,EAAA,EAAA,IAAA,CAAC,OAAD,CAEI,UAAW,EAAA,GAAG,EAAA,QAAO,MAAO,GAAc,EAAA,QAAO,QAAQ,EACzD,EAAG,EAAM,EACT,SAAS,UACT,MAAO,EAAY,CAAE,KAAM,CAAU,EAAI,IAAA,GACzC,UAAS,EAAM,GACf,SAAU,EAAW,EAAI,IAAA,GACzB,KAAM,EAAW,SAAW,IAAA,GAC5B,aAAY,EAAM,KAClB,eAAc,EAAW,EAAa,IAAA,GACtC,QAAS,MAAiB,EAAS,EAAM,EAAE,EAAI,IAAA,GAC/C,YACI,EACO,GAAM,EAAO,CAAE,GAAI,EAAM,GAAI,KAAM,EAAM,IAAK,EAAG,CAAC,EACnD,IAAA,GAEV,aAAc,EAAc,EAAU,IAAA,GACtC,UACI,EACO,GAAM,EACC,EAAE,MAAQ,SAAW,EAAE,MAAQ,OAC/B,EAAE,eAAe,EACjB,EAAS,EAAM,EAAE,EAEzB,EACA,IAAA,EAEb,EA3BQ,EAAM,EA2Bd,CAET,CAAC,EAvCmB,KAyCrB,CACC,EACA,EACA,EACA,EACA,EACA,EACA,EACA,EACA,EACA,EACA,CACJ,CAAC,EAED,OACI,EAAA,EAAA,KAAA,CAAC,MAAD,CACI,IAAK,EACL,UAAW,EAAA,GAAG,EAAA,QAAO,IAAK,CAAS,EACnC,MAAO,CAAE,SAAQ,GAAG,CAAM,EAC1B,KAAK,QACL,aAAY,EACZ,GAAI,EANR,SAAA,CAQM,GAGE,EAAA,EAAA,IAAA,CAAC,MAAD,CACI,UAAW,EAAA,QAAO,IAClB,MAAM,OACE,SACR,QAAS,OAAO,EAAM,GAAG,IACzB,oBAAoB,gBACpB,GAAI,EAAQ,SAEZ,UAAA,EAAA,EAAA,KAAA,CAAC,IAAD,CAAG,UAAW,EAAQ,WAAa,IAAA,GAAnC,SAAA,CACK,EAEA,GACG,EAAO,MAAM,IAAK,IACd,EAAA,EAAA,IAAA,CAAC,OAAD,CAEI,UAAW,EAAA,QAAO,MAClB,EAAG,EAAM,SAAS,EAClB,EAAG,EAAM,SAAS,EAClB,WAAW,SACX,iBAAiB,UAEhB,SAAA,EAAM,EACL,EARG,SAAS,EAAM,IAQlB,CACT,EAEJ,GAAW,EAAQ,OAAS,IACzB,EAAA,EAAA,IAAA,CAAC,EAAA,WAAD,CACI,WAAY,EAAO,WACV,UACM,eAClB,CAAA,CAEN,GACF,CAAA,GAnCL,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,QAAU,SAAA,GAAkB,kBAAwB,CAAA,EAsC9E,EAAQ,gBACL,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,KAAK,SAAS,UAAW,EAAA,QAAO,UAAW,QAAS,EAAQ,MAAO,SAAA,OAEnE,CAAA,EAGX,GAAe,IACZ,EAAA,EAAA,IAAA,CAAC,EAAA,WAAD,CAAY,EAAG,EAAM,EAAG,EAAG,EAAM,EAC5B,SAAA,EACG,EAAc,CACV,GAAI,EAAM,KAAK,GACf,KAAM,EAAM,KAAK,KACjB,MAAO,IAAS,EAAM,KAAK,GAC/B,CAAC,GAED,EAAA,EAAA,IAAA,CAAC,EAAD,CACI,GAAI,EAAM,KAAK,GACf,KAAM,EAAM,KAAK,KACjB,MAAO,IAAS,EAAM,KAAK,GAC9B,CAAA,CAEG,CAAA,CAEf,GAEb,CAGA,SAAS,EAAiB,CAAE,KAAI,OAAM,SAAmD,CACrF,IAAM,EAAQ,EAAA,SAAS,CAAE,EACzB,OACI,EAAA,EAAA,KAAA,CAAA,EAAA,SAAA,CAAA,SAAA,EACI,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,aAAvB,SAAA,CACK,EAAK,KAAG,EAAG,GACX,CACL,CAAA,GAAA,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,YAAvB,SAAA,CACK,EAAQ,GAAG,EAAM,OAAO,KAAK,EAAM,OAAO,OAAO,UAAY,EAC7D,OAAO,GAAU,SAAW,MAAM,IAAU,EAC5C,CACP,CAAA,CAAA,CAAA,CAAA,CAEV"}