{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "flow-map",
  "title": "Migration Flow Map",
  "description": "A page of interprovincial migration flows between the capitals, from Statistics Canada Table 17-10-0022-01 (2021/2022 to 2025/2026, Statistics Canada Open Licence), with a year scrubber that animates the flows and each province's net gain or loss. Calls no API beyond the map.",
  "registryDependencies": [
    "https://registry.unmap.dev/r/map.json",
    "https://registry.unmap.dev/r/map-context.json",
    "https://registry.unmap.dev/r/flow-layer.json",
    "https://registry.unmap.dev/r/time-slider.json",
    "card",
    "badge",
    "button",
    "alert"
  ],
  "files": [
    {
      "path": "registry/blocks/flow-map/page.tsx",
      "content": "/**\n * /flow-map: the migration flow map block as a Next.js App Router page, full-bleed (the block keeps\n * its own gutter). The key comes from NEXT_PUBLIC_UNMAP_KEY in .env.local; installing the block\n * added the empty line, paste your key.\n */\nimport { FlowMap } from \"@/components/ui/flow-map\";\n\nexport const metadata = { title: \"Follow interprovincial migration\" };\n\nexport default function FlowMapPage() {\n  return (\n    <main className=\"h-dvh\">\n      <FlowMap apiKey={process.env.NEXT_PUBLIC_UNMAP_KEY ?? \"\"} className=\"h-full\" />\n    </main>\n  );\n}\n",
      "type": "registry:page",
      "target": "app/flow-map/page.tsx"
    },
    {
      "path": "registry/blocks/flow-map/flow-map.tsx",
      "content": "\"use client\";\n\n/**\n * <FlowMap> - a migration flow map in one component. A full-bleed map of Canada with a floating\n * panel (a bottom sheet on a phone): Statistics Canada's interprovincial migration estimates drawn\n * as flows between the capitals, and a year scrubber that makes them breathe. Play or step through\n * the years and the flow widths move from one year to the next while each province's net gain or\n * loss grows and shrinks on one scale. Point at a flow, a capital or a row for its numbers; pick a\n * province to draw only its flows.\n *\n * Composes the registry's map, flow-layer and time-slider items on your shadcn Card, Badge, Button\n * and Alert, in your tokens. The data ships with the block (lib/flow-map-data, Statistics Canada\n * Open Licence, credited in the panel), so it calls no API beyond the map's own style and tiles.\n * The slider names no style layer: flow-layer re-sums the flows for the chosen year itself.\n *\n * Installed by `npx shadcn add` with its page at app/flow-map/page.tsx. Edit freely.\n */\n\nimport { useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport { Alert, AlertDescription, AlertTitle } from \"@/components/ui/alert\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { FlowLayer } from \"@/components/ui/flow-layer\";\nimport { Map } from \"@/components/ui/map\";\nimport { TimeSlider } from \"@/components/ui/time-slider\";\nimport { useMap } from \"@/lib/map-context\";\nimport {\n  MIGRATION,\n  MIGRATION_LOCATIONS,\n  MIGRATION_PERIODS,\n  MIGRATION_PRELIMINARY,\n  MIGRATION_SOURCE,\n  type MigrationLocation,\n  type MigrationRow,\n} from \"@/lib/flow-map-data\";\n\n// --- flow-map helpers (kept identical across both lanes) ---\nexport type FlowMapLang = \"en\" | \"fr\";\n\n/** One table row as flow-layer reads it; `time` is the period's first year. */\nexport interface MigrationFlow {\n  origin: string;\n  dest: string;\n  count: number;\n  time: number;\n}\n\n/** A capital as flow-layer reads it, named in the page's language. */\nexport interface MigrationPlace {\n  id: string;\n  name: string;\n  lat: number;\n  lon: number;\n}\n\n/** One province's or territory's year: who arrived, who left, and the difference. */\nexport interface ProvinceBalance {\n  id: string;\n  in: number;\n  out: number;\n  net: number;\n}\n\n/** What the pointer or the keyboard is on: a province, or one flow by its ends. */\nexport type FlowMapFocus = { kind: \"place\"; id: string } | { kind: \"flow\"; origin: string; dest: string };\n\n/** A box on screen, as getBoundingClientRect() gives it. */\nexport interface FlowMapRect {\n  left: number;\n  top: number;\n  right: number;\n  bottom: number;\n  width: number;\n  height: number;\n}\n\n/** Camera padding in pixels, as fitBounds takes it. */\nexport interface FlowMapPadding {\n  top: number;\n  right: number;\n  bottom: number;\n  left: number;\n}\n\n/** The block's own map id, so a map elsewhere on the page never answers for it. */\nexport const FLOW_MAP_ID = \"flow-map\";\n/** The flow layer's source id and layer-id prefix; its capitals are the `-locations` source. */\nexport const FLOW_LAYER_ID = \"flow-map\";\nexport const FLOW_LOCATIONS_SOURCE = `${FLOW_LAYER_ID}-locations`;\n\n/** The slider filters no style layer: flow-layer re-sums the flows for the year itself. */\nexport const NO_LAYERS: string[] = [];\n\n/** All of Canada: where the map opens before it frames the capitals. */\nexport const CANADA_VIEW: { center: [number, number]; zoom: number } = { center: [-96, 58], zoom: 3 };\n\n/**\n * The thirteen capitals and the arcs that bow between them, for fitBounds. The south edge sits\n * just below Toronto, so the frame spends its height on Canada rather than on the arcs' bellies.\n */\nexport const CAPITALS_BOUNDS: [[number, number], [number, number]] = [\n  [-137, 42.5],\n  [-51, 66],\n];\n\n/** How far the flows bow: less than flow-layer's 0.25, so the long east-west arcs stay near Canada. */\nexport const FLOW_CURVATURE = 0.15;\n\n/** The panel's inset from the map edge, and the gutter the camera keeps. */\nexport const GUTTER = 24;\n/** How long the flow widths take to move from one year to the next. Reduced motion jumps. */\nexport const TWEEN_MS = 600;\n/** The least map, in pixels, a padded camera must still have to frame something. */\nconst MIN_FRAME = 120;\n\n/** The licences the credit links, in the reader's language. */\nexport const FLOW_MAP_LICENCES = {\n  statcan: {\n    en: \"https://www.statcan.gc.ca/en/terms-conditions/open-licence\",\n    fr: \"https://www.statcan.gc.ca/fr/avis/licence-ouverte\",\n  },\n  ogl: {\n    en: \"https://open.canada.ca/en/open-government-licence-canada\",\n    fr: \"https://ouvert.canada.ca/fr/licence-du-gouvernement-ouvert-canada\",\n  },\n};\n\n/** Where a reader gets an unmap key: the no-key state's first action. */\nexport const KEY_URL = \"https://unmap.dev/dashboard\";\n\nexport const FLOW_MAP_COPY = {\n  en: {\n    title: \"Interprovincial migration\",\n    intro: \"Who moved between provinces and territories, year by year. Play the years, or pick a province to see only its flows.\",\n    total: (count: string, period: string) => `${count} people moved between provinces and territories in ${period}.`,\n    preliminary: \"Preliminary estimates.\",\n    preliminaryBadge: \"Preliminary\",\n    moved: \"people moved\",\n    movedCaption: (period: string) => `between provinces and territories in ${period}`,\n    net: \"net\",\n    placeCaption: (name: string, period: string) => `${name}, ${period}`,\n    hint: \"Point at a flow or a capital for its numbers.\",\n    hintTouch: \"Tap a flow or a capital for its numbers.\",\n    flow: (from: string, to: string, count: string) => `${from} to ${to}: ${count} people`,\n    place: (name: string, arrived: string, left: string) => `${name}: ${arrived} arrived, ${left} left`,\n    list: \"Net migration\",\n    listLabel: (period: string) => `Net migration by province and territory, ${period}`,\n    gained: \"Gained\",\n    lost: \"Lost\",\n    showAll: \"Show all provinces\",\n    showList: \"Show provinces\",\n    hideList: \"Hide provinces\",\n    showing: (name: string) => `Showing flows to and from ${name}.`,\n    sayNet: (net: string) => `Net migration: ${net}.`,\n    licences: \"Licences:\",\n    statcanLicence: \"Statistics Canada Open Licence\",\n    oglLicence: \"Open Government Licence – Canada\",\n    noKeyTitle: \"Add your unmap key to load the map.\",\n    getKey: \"Get a key\",\n    noKey:\n      'Pass apiKey. On Next.js, set NEXT_PUBLIC_UNMAP_KEY in .env.local. On Nuxt, set NUXT_PUBLIC_UNMAP_KEY in .env. If you installed with unmap add, nuxt.config already declares runtimeConfig: { public: { unmapKey: \"\" } }; otherwise add it there. Then restart the dev server.',\n  },\n  fr: {\n    title: \"Migration interprovinciale\",\n    intro:\n      \"Qui a changé de province ou de territoire, année après année. Faites défiler les années, ou choisissez une province pour ne voir que ses flux.\",\n    total: (count: string, period: string) => `${count} personnes ont changé de province ou de territoire en ${period}.`,\n    preliminary: \"Estimations provisoires.\",\n    preliminaryBadge: \"Provisoire\",\n    moved: \"personnes\",\n    movedCaption: (period: string) => `ont changé de province ou de territoire en ${period}`,\n    net: \"solde\",\n    placeCaption: (name: string, period: string) => `${name}, ${period}`,\n    hint: \"Pointez un flux ou une capitale pour voir ses chiffres.\",\n    hintTouch: \"Touchez un flux ou une capitale pour voir ses chiffres.\",\n    flow: (from: string, to: string, count: string) => `${from} vers ${to} : ${count} personnes`,\n    place: (name: string, arrived: string, left: string) => `${name} : ${arrived} arrivées, ${left} départs`,\n    list: \"Solde migratoire\",\n    listLabel: (period: string) => `Solde migratoire par province et territoire, ${period}`,\n    gained: \"Gain\",\n    lost: \"Perte\",\n    showAll: \"Voir toutes les provinces\",\n    showList: \"Voir les provinces\",\n    hideList: \"Masquer les provinces\",\n    showing: (name: string) => `Flux affichés : ${name}.`,\n    sayNet: (net: string) => `Solde migratoire : ${net}.`,\n    licences: \"Licences :\",\n    statcanLicence: \"Licence ouverte de Statistique Canada\",\n    oglLicence: \"Licence du gouvernement ouvert – Canada\",\n    noKeyTitle: \"Ajoutez votre clé unmap pour charger la carte.\",\n    getKey: \"Obtenir une clé\",\n    noKey:\n      'Passez apiKey. Avec Next.js, définissez NEXT_PUBLIC_UNMAP_KEY dans .env.local. Avec Nuxt, définissez NUXT_PUBLIC_UNMAP_KEY dans .env. Si vous avez installé le bloc avec unmap add, nuxt.config déclare déjà runtimeConfig: { public: { unmapKey: \"\" } } ; sinon, ajoutez-le. Redémarrez ensuite le serveur de développement.',\n  },\n};\n\n/** \"2023/2024\" to 2023. */\nexport function periodStart(period: string): number {\n  return Number(period.slice(0, 4));\n}\n\n/** 2023 to \"2023/2024\", Statistics Canada's July-to-June reference period. */\nexport function periodLabel(start: number): string {\n  return `${start}/${start + 1}`;\n}\n\nexport function migrationFlows(data: Record<string, MigrationRow[]>): MigrationFlow[] {\n  return Object.entries(data).flatMap(([period, rows]) =>\n    rows.map(([origin, dest, count]) => ({ origin, dest, count, time: periodStart(period) })),\n  );\n}\n\nexport function migrationPlaces(locations: MigrationLocation[], lang: FlowMapLang): MigrationPlace[] {\n  return locations.map((location) => ({\n    id: location.id,\n    name: lang === \"fr\" ? location.name_fr : location.name,\n    lat: location.lat,\n    lon: location.lon,\n  }));\n}\n\n/** Everyone who moved between provinces and territories in the period starting `year`. */\nexport function yearTotal(flows: MigrationFlow[], year: number): number {\n  return flows.reduce((sum, flow) => (flow.time === year ? sum + flow.count : sum), 0);\n}\n\n/** Each place's year, in the order `places` gives (Statistics Canada's, east to west, then north). */\nexport function provinceBalances(flows: MigrationFlow[], year: number, places: Array<{ id: string }>): ProvinceBalance[] {\n  // `globalThis.Map`: the React lane imports a `Map` component.\n  const totals = new globalThis.Map(places.map((place) => [place.id, { in: 0, out: 0 }]));\n  for (const flow of flows) {\n    if (flow.time !== year) continue;\n    const origin = totals.get(flow.origin);\n    const dest = totals.get(flow.dest);\n    if (origin) origin.out += flow.count;\n    if (dest) dest.in += flow.count;\n  }\n  return places.map(({ id }) => {\n    const total = totals.get(id)!;\n    return { id, in: total.in, out: total.out, net: total.in - total.out };\n  });\n}\n\n/** The largest net gain or loss of any place in any year: one scale, so a bar means the same in every year. */\nexport function largestNet(flows: MigrationFlow[], places: Array<{ id: string }>): number {\n  const years = [...new Set(flows.map((flow) => flow.time))];\n  return years.reduce(\n    (largest, year) => provinceBalances(flows, year, places).reduce((most, balance) => Math.max(most, Math.abs(balance.net)), largest),\n    0,\n  );\n}\n\n/** How many moved from `origin` to `dest` in the period starting `year`. */\nexport function pairCount(flows: MigrationFlow[], year: number, origin: string, dest: string): number {\n  return flows.reduce((sum, flow) => (flow.time === year && flow.origin === origin && flow.dest === dest ? sum + flow.count : sum), 0);\n}\n\n/** The flows to and from one place, or every flow (the same array) when none is picked. */\nexport function touching(flows: MigrationFlow[], id: string | null): MigrationFlow[] {\n  return id === null ? flows : flows.filter((flow) => flow.origin === id || flow.dest === id);\n}\n\n/**\n * The flows part way from the `from` flows to the `to` flows, pair by pair: `t` 0 is `from`, 1 is\n * `to`. A pair in only one of them fades from or to nothing. Every flow is stamped `year`, so the\n * layer's time range for that year keeps them all. `from` can be any snapshot, such as the widths\n * on screen part way through another blend.\n */\nexport function blendPairs(from: MigrationFlow[], to: MigrationFlow[], year: number, t: number): MigrationFlow[] {\n  const pairs = new globalThis.Map<string, MigrationFlow>();\n  const add = (flows: MigrationFlow[], weight: number) => {\n    for (const flow of flows) {\n      const key = `${flow.origin}>${flow.dest}`;\n      const pair = pairs.get(key) ?? { origin: flow.origin, dest: flow.dest, count: 0, time: year };\n      pair.count += flow.count * weight;\n      pairs.set(key, pair);\n    }\n  };\n  add(from, 1 - t);\n  add(to, t);\n  return [...pairs.values()];\n}\n\n/** The flows part way from year `from` to year `to` of one table: `t` 0 is `from`, 1 is `to`. */\nexport function blendFlows(flows: MigrationFlow[], from: number, to: number, t: number): MigrationFlow[] {\n  const year = (start: number) => flows.filter((flow) => flow.time === start);\n  return from === to ? year(to) : blendPairs(year(from), year(to), to, t);\n}\n\n/** The largest single flow of any year: the top of a width scale every year shares. */\nexport function largestPair(flows: MigrationFlow[]): number {\n  return flows.reduce((largest, flow) => Math.max(largest, flow.count), 0);\n}\n\n/** The most anyone moved in and out of one place in any year: the top of a circle scale every year shares. */\nexport function largestTotal(flows: MigrationFlow[], places: Array<{ id: string }>): number {\n  const years = [...new Set(flows.map((flow) => flow.time))];\n  return years.reduce(\n    (largest, year) => provinceBalances(flows, year, places).reduce((most, balance) => Math.max(most, balance.in + balance.out), largest),\n    0,\n  );\n}\n\nexport function formatCount(count: number, lang: FlowMapLang): string {\n  return new Intl.NumberFormat(lang === \"fr\" ? \"fr-CA\" : \"en-CA\").format(count);\n}\n\n/** A net with its sign spelled out: \"+21,662\", \"−18,015\" (a true minus), \"0\". */\nexport function formatNet(net: number, lang: FlowMapLang): string {\n  const size = formatCount(Math.abs(net), lang);\n  return net > 0 ? `+${size}` : net < 0 ? `−${size}` : size;\n}\n\n/**\n * What the pointer is over, in words. A flow line carries origin, dest and count; a place carries\n * id, in and out (flow-layer's feature properties). Anything else (an arrowhead) is null.\n */\nexport function describeHover(properties: Record<string, unknown>, places: MigrationPlace[], lang: FlowMapLang): string | null {\n  const copy = FLOW_MAP_COPY[lang];\n  const name = (id: unknown) => places.find((place) => place.id === id)?.name ?? String(id);\n  if (typeof properties.origin === \"string\" && typeof properties.dest === \"string\") {\n    return copy.flow(name(properties.origin), name(properties.dest), formatCount(Number(properties.count), lang));\n  }\n  if (typeof properties.id === \"string\" && properties.in !== undefined && properties.out !== undefined) {\n    return copy.place(name(properties.id), formatCount(Number(properties.in), lang), formatCount(Number(properties.out), lang));\n  }\n  return null;\n}\n\n/** What a flow-layer feature is, from its properties: a flow, a capital, or nothing to describe. */\nexport function focusOf(properties: Record<string, unknown>): FlowMapFocus | null {\n  if (typeof properties.origin === \"string\" && typeof properties.dest === \"string\") {\n    return { kind: \"flow\", origin: properties.origin, dest: properties.dest };\n  }\n  if (typeof properties.id === \"string\" && properties.in !== undefined) return { kind: \"place\", id: properties.id };\n  return null;\n}\n\n/**\n * The focus in words, with the year's numbers from the whole table: flow-layer's own in and out\n * cover only the flows it draws, and its counts move while the widths animate.\n */\nexport function describeFocus(focus: FlowMapFocus, flows: MigrationFlow[], year: number, places: MigrationPlace[], lang: FlowMapLang): string | null {\n  if (focus.kind === \"flow\") {\n    return describeHover({ origin: focus.origin, dest: focus.dest, count: pairCount(flows, year, focus.origin, focus.dest) }, places, lang);\n  }\n  const balance = provinceBalances(flows, year, [{ id: focus.id }])[0]!;\n  return describeHover({ id: focus.id, in: balance.in, out: balance.out }, places, lang);\n}\n\n/**\n * Camera padding that keeps what the camera frames clear of the floating panel: its width on the\n * left when it floats beside the map, its height at the bottom when it is a sheet across it. An\n * even gutter when nothing is measured, or when the panel would leave the camera no map.\n */\nexport function panelPadding(map: FlowMapRect, panel: FlowMapRect | null, gutter = GUTTER): FlowMapPadding {\n  const even = { top: gutter, right: gutter, bottom: gutter, left: gutter };\n  if (!panel || panel.width <= 0 || panel.height <= 0 || map.width <= 0 || map.height <= 0) return even;\n  const sheet = panel.width >= map.width * 0.6;\n  const padding = sheet\n    ? { ...even, bottom: Math.round(map.bottom - panel.top + gutter) }\n    : { ...even, left: Math.round(panel.right - map.left + gutter) };\n  const room = sheet ? map.height - padding.top - padding.bottom : map.width - padding.left - padding.right;\n  return room >= MIN_FRAME ? padding : even;\n}\n\n/** The scroll position that shows `row` whole in `view`, moving as little as it can. */\nexport function listScrollTop(view: { scrollTop: number; clientHeight: number }, row: { offsetTop: number; offsetHeight: number }): number {\n  if (row.offsetTop < view.scrollTop) return row.offsetTop;\n  const bottom = row.offsetTop + row.offsetHeight;\n  return bottom > view.scrollTop + view.clientHeight ? bottom - view.clientHeight : view.scrollTop;\n}\n\n/** True on a touch screen, where there is no pointer to hover with: the hint says tap. Read after mount. */\nexport function coarsePointer(): boolean {\n  return typeof window !== \"undefined\" && window.matchMedia?.(\"(pointer: coarse)\")?.matches === true;\n}\n\n/** True when the reader asked for less motion: the years jump rather than blend. */\nexport function prefersReducedMotion(): boolean {\n  return typeof window !== \"undefined\" && window.matchMedia?.(\"(prefers-reduced-motion: reduce)\")?.matches === true;\n}\n\n/** Fast start, soft landing. */\nexport function easeOut(t: number): number {\n  return 1 - (1 - t) ** 3;\n}\n// --- end flow-map helpers ---\n\nexport interface FlowMapProps {\n  /** unmap API key. The page passes NEXT_PUBLIC_UNMAP_KEY. With none, the block asks for one and loads nothing. */\n  apiKey: string;\n  /** Gateway base URL. Defaults to the unmap production gateway. */\n  gateway?: string;\n  /** Language of the copy, the numbers and the place names. Defaults to \"en\". */\n  lang?: FlowMapLang;\n  /** Draw only the N largest flows of the year. Defaults to 40 of the 156. */\n  topN?: number;\n  className?: string;\n}\n\nexport function FlowMap({ apiKey, gateway, lang = \"en\", topN = 40, className }: FlowMapProps) {\n  if (!apiKey.trim()) {\n    const copy = FLOW_MAP_COPY[lang];\n    // Where the map would be: the block's gutter, the Alert at a readable width, the first action first.\n    return (\n      <div\n        data-unmap-block=\"flow-map\"\n        data-state=\"no-key\"\n        className={[\"flex h-full min-h-[480px] items-start justify-center bg-muted/40 p-4 sm:items-center sm:p-6\", className].filter(Boolean).join(\" \")}\n      >\n        <Alert role=\"alert\" className=\"w-full max-w-md motion-safe:animate-in motion-safe:fade-in-0 motion-safe:duration-200\">\n          <AlertTitle className=\"line-clamp-none\">{copy.noKeyTitle}</AlertTitle>\n          <AlertDescription className=\"flex flex-col items-start gap-3\">\n            <a className=\"font-medium text-primary underline underline-offset-4\" href={KEY_URL} target=\"_blank\" rel=\"noreferrer\">\n              {copy.getKey}\n            </a>\n            <p>{copy.noKey}</p>\n          </AlertDescription>\n        </Alert>\n      </div>\n    );\n  }\n  return (\n    <div data-unmap-block=\"flow-map\" className={[\"relative h-full min-h-[480px]\", className].filter(Boolean).join(\" \")}>\n      <Map\n        id={FLOW_MAP_ID}\n        apiKey={apiKey}\n        gateway={gateway}\n        style=\"muted\"\n        center={CANADA_VIEW.center}\n        zoom={CANADA_VIEW.zoom}\n        lang={lang}\n        className=\"h-full\"\n      >\n        <Migration lang={lang} topN={topN} />\n      </Map>\n    </div>\n  );\n}\n\n/** The whole table as flows, built once: flow-layer rebuilds its model whenever it is handed a new array. */\nconst FLOWS = migrationFlows(MIGRATION);\nconst FIRST_YEAR = periodStart(MIGRATION_PERIODS[0]!);\nconst LAST_YEAR = periodStart(MIGRATION_PERIODS[MIGRATION_PERIODS.length - 1]!);\nconst LARGEST_NET = largestNet(FLOWS, MIGRATION_LOCATIONS);\n/** One width scale and one circle scale for every year, so a width means the same count in each. */\nconst COUNT_DOMAIN: [number, number] = [0, largestPair(FLOWS)];\nconst TOTAL_DOMAIN: [number, number] = [0, largestTotal(FLOWS, MIGRATION_LOCATIONS)];\n\n/** A year change in flight: widths blend from the `start` snapshot to year `to`; `t` runs 0 to 1. */\ntype Tween = { start: MigrationFlow[]; to: number; t: number };\ntype Hover = { focus: FlowMapFocus; from: \"map\" | \"list\" };\n\n/**\n * The flows to draw for `year`: `flows` itself at rest, and a blend from the widths on screen to\n * this year, a new array each frame, while the widths move. Reduced motion jumps.\n */\nfunction useYearBlend(flows: MigrationFlow[], year: number): MigrationFlow[] {\n  const [tween, setTween] = useState<Tween>({ start: [], to: year, t: 1 });\n  const running = tween.t < 1;\n  const blended = useMemo(\n    () => (running ? blendPairs(tween.start, flows.filter((flow) => flow.time === tween.to), tween.to, easeOut(tween.t)) : flows),\n    [flows, running, tween],\n  );\n  // A new year starts the blend in the same render, so no frame shows the new year unblended, and\n  // it starts from what is on screen, so a change mid-blend never jumps.\n  if (tween.to !== year) {\n    setTween({ start: blended.filter((flow) => flow.time === tween.to), to: year, t: prefersReducedMotion() ? 1 : 0 });\n  }\n  useEffect(() => {\n    if (!running) return;\n    const start = performance.now();\n    let frame = requestAnimationFrame(function step() {\n      const t = Math.min(1, (performance.now() - start) / TWEEN_MS);\n      setTween((current) => ({ ...current, t }));\n      if (t < 1) frame = requestAnimationFrame(step);\n    });\n    return () => cancelAnimationFrame(frame);\n  }, [running, tween.start, tween.to]);\n  return tween.to === year ? blended : flows;\n}\n\n/** The flows, the scrubber and the list. It renders inside <Map>, where useMap() can see it. */\nfunction Migration({ lang, topN }: { lang: FlowMapLang; topN: number }) {\n  const copy = FLOW_MAP_COPY[lang];\n  const map = useMap(FLOW_MAP_ID);\n  const places = useMemo(() => migrationPlaces(MIGRATION_LOCATIONS, lang), [lang]);\n  const [year, setYear] = useState(LAST_YEAR);\n  const [hover, setHover] = useState<Hover | null>(null);\n  const [selected, setSelected] = useState<string | null>(null);\n  const [expanded, setExpanded] = useState(false);\n  // Read after mount: the server cannot know the pointer, so the first pass says \"point\".\n  const [touch, setTouch] = useState(false);\n  const panelRef = useRef<HTMLDivElement | null>(null);\n  const scrollRef = useRef<HTMLDivElement | null>(null);\n  const listId = useId();\n\n  const period = periodLabel(year);\n  const preliminary = period === MIGRATION_PRELIMINARY;\n  // A row under the pointer or the keyboard previews its province's flows; a pick keeps them.\n  const lit = hover?.from === \"list\" && hover.focus.kind === \"place\" ? hover.focus.id : null;\n  const shown = useMemo(() => touching(FLOWS, lit ?? selected), [lit, selected]);\n  const drawn = useYearBlend(shown, year);\n  const timeRange = useMemo<[number, number]>(() => [year, year], [year]);\n  const balances = useMemo(() => provinceBalances(FLOWS, year, MIGRATION_LOCATIONS), [year]);\n  const nameOf = (id: string) => places.find((place) => place.id === id)?.name ?? id;\n\n  // The province the headline speaks for: the one under the pointer, else the one picked.\n  const pointed = hover?.focus ?? null;\n  const headlinePlace = pointed?.kind === \"place\" ? pointed.id : selected;\n  const headlineBalance = headlinePlace ? balances.find((balance) => balance.id === headlinePlace) : undefined;\n  const readoutFocus: FlowMapFocus | null = pointed ?? (selected ? { kind: \"place\", id: selected } : null);\n  const readout = readoutFocus ? describeFocus(readoutFocus, FLOWS, year, places, lang) : null;\n  const hoveredRow = pointed?.kind === \"place\" ? pointed.id : null;\n  const totalSentence = `${copy.total(formatCount(yearTotal(FLOWS, year), lang), period)}${preliminary ? ` ${copy.preliminary}` : \"\"}`;\n  const selectedBalance = selected ? balances.find((balance) => balance.id === selected) : undefined;\n  const announcement = selected\n    ? `${totalSentence} ${copy.showing(nameOf(selected))}${selectedBalance ? ` ${copy.sayNet(formatNet(selectedBalance.net, lang))}` : \"\"}`\n    : totalSentence;\n\n  useEffect(() => setTouch(coarsePointer()), []);\n\n  // The camera frames every capital clear of the panel, without a move to watch: now, again when\n  // the map resizes and once the panel has slid in, until the reader moves the map themselves.\n  useEffect(() => {\n    if (!map) return;\n    let moved = false;\n    const fit = () => {\n      if (moved) return;\n      const padding = panelPadding(map.getContainer().getBoundingClientRect(), panelRef.current?.getBoundingClientRect() ?? null);\n      map.fitBounds(CAPITALS_BOUNDS, { padding, duration: 0 });\n    };\n    // The camera's own moves carry no originalEvent; a drag, a wheel or a key does.\n    const onMoveStart = (event?: { originalEvent?: unknown }) => {\n      if (event?.originalEvent) moved = true;\n    };\n    // Measured again once the panel has slid in: mid-slide it sits off its final place.\n    const panel = panelRef.current;\n    const onSlidIn = (event: Event) => {\n      if (event.target === panel) fit();\n    };\n    fit();\n    map.on(\"resize\", fit);\n    map.on(\"movestart\", onMoveStart as never);\n    panel?.addEventListener(\"animationend\", onSlidIn);\n    return () => {\n      map.off(\"resize\", fit);\n      map.off(\"movestart\", onMoveStart as never);\n      panel?.removeEventListener(\"animationend\", onSlidIn);\n    };\n  }, [map]);\n\n  // The row's capital lights too, through the layer's own hover state.\n  useEffect(() => {\n    if (!map || !lit || !map.getSource(FLOW_LOCATIONS_SOURCE)) return;\n    map.setFeatureState({ source: FLOW_LOCATIONS_SOURCE, id: lit }, { hover: true });\n    return () => {\n      if (map.getSource(FLOW_LOCATIONS_SOURCE)) map.setFeatureState({ source: FLOW_LOCATIONS_SOURCE, id: lit }, { hover: false });\n    };\n  }, [map, lit]);\n\n  // A row lit from the map scrolls into the list's view. Only the list scrolls, never the page.\n  useEffect(() => {\n    const scroller = scrollRef.current;\n    if (hover?.from !== \"map\" || hover.focus.kind !== \"place\" || !scroller) return;\n    const id = hover.focus.id;\n    const row = [...scroller.querySelectorAll<HTMLElement>(\"[data-province]\")].find((element) => element.getAttribute(\"data-province\") === id);\n    if (!row) return;\n    const top = listScrollTop(scroller, row);\n    if (top !== scroller.scrollTop) scroller.scrollTop = top;\n  }, [hover]);\n\n  function onMapHover(event: { feature: { properties: Record<string, unknown> } } | null) {\n    const focus = event ? focusOf(event.feature.properties) : null;\n    setHover((current) => (focus ? { focus, from: \"map\" } : current?.from === \"map\" ? null : current));\n  }\n\n  function onMapClick(event: { feature: { properties: Record<string, unknown> } }) {\n    const focus = focusOf(event.feature.properties);\n    if (focus?.kind === \"place\") pick(focus.id);\n    // A tap on a flow describes it: a touch screen has no hover to do it.\n    else if (focus) setHover({ focus, from: \"map\" });\n  }\n\n  function pick(id: string | null) {\n    setSelected((current) => (id === null || current === id ? null : id));\n  }\n\n  function enterRow(id: string) {\n    setHover({ focus: { kind: \"place\", id }, from: \"list\" });\n  }\n\n  function leaveRow() {\n    setHover((current) => (current?.from === \"list\" ? null : current));\n  }\n\n  return (\n    <>\n      <FlowLayer\n        mapId={FLOW_MAP_ID}\n        id={FLOW_LAYER_ID}\n        locations={places}\n        flows={drawn}\n        timeRange={timeRange}\n        topN={topN}\n        curvature={FLOW_CURVATURE}\n        countDomain={COUNT_DOMAIN}\n        totalDomain={TOTAL_DOMAIN}\n        onHover={onMapHover}\n        onClick={onMapClick}\n      />\n      <p data-testid=\"flow-map-announce\" aria-live=\"polite\" className=\"sr-only\">\n        {announcement}\n      </p>\n      {/* A card floating on the map at 24 px; below `sm`, a sheet across the bottom that clears the attribution, leaves the map most of the screen, and grows, below the map controls, when the list is shown. */}\n      <div\n        ref={panelRef}\n        data-flow-map-panel=\"\"\n        data-expanded={expanded}\n        className=\"absolute inset-x-4 bottom-10 z-10 flex max-h-[55%] flex-col max-sm:data-[expanded=true]:max-h-[calc(100%-9rem)] sm:inset-x-auto sm:top-6 sm:bottom-auto sm:left-6 sm:max-h-[calc(100%-3rem)] sm:w-[22rem] motion-safe:animate-in motion-safe:fade-in-0 motion-safe:slide-in-from-bottom-4 motion-safe:duration-300 sm:motion-safe:slide-in-from-bottom-0 sm:motion-safe:slide-in-from-left-4\"\n      >\n        <Card className=\"flex min-h-0 flex-1 flex-col\">\n          <CardHeader>\n            <CardTitle role=\"heading\" aria-level={2}>\n              {copy.title}\n            </CardTitle>\n            <CardDescription className=\"max-sm:hidden\">{copy.intro}</CardDescription>\n          </CardHeader>\n          {/* The scrubber stays put. On a wide screen the headline and the readout stay put too and only\n              the list scrolls; in a phone's short sheet everything below the scrubber scrolls, so the\n              list and the credit are always reachable. */}\n          <CardContent className=\"flex min-h-0 flex-1 flex-col gap-4\">\n            <TimeSlider\n              mapId={FLOW_MAP_ID}\n              layers={NO_LAYERS}\n              property=\"year\"\n              min={FIRST_YEAR}\n              max={LAST_YEAR}\n              value={year}\n              onValueChange={(value) => setYear(typeof value === \"number\" ? value : value[0])}\n              format={periodLabel}\n              lang={lang}\n              interval={1500}\n              className=\"shrink-0\"\n            />\n            <div data-flow-map-body=\"\" className=\"-mx-2 flex min-h-0 flex-1 flex-col gap-4 px-2 max-sm:overflow-y-auto max-sm:overscroll-contain\">\n              {/* The headline, always there and always this tall, so nothing jumps as the years play. */}\n              <div data-testid=\"flow-map-headline\" className=\"flex min-h-16 shrink-0 flex-col justify-center gap-1\">\n                <p className=\"flex flex-wrap items-baseline gap-x-2 gap-y-1\">\n                  <span className=\"text-3xl leading-none font-semibold tracking-tight tabular-nums\">\n                    {headlineBalance ? formatNet(headlineBalance.net, lang) : formatCount(yearTotal(FLOWS, year), lang)}\n                  </span>{\" \"}\n                  <span className=\"text-sm text-muted-foreground\">{headlineBalance ? copy.net : copy.moved}</span>\n                  {preliminary ? (\n                    <>\n                      {\" \"}\n                      <Badge variant=\"outline\" className=\"self-center\">\n                        {copy.preliminaryBadge}\n                      </Badge>\n                    </>\n                  ) : null}\n                </p>\n                <p className=\"text-sm text-muted-foreground\">\n                  {headlineBalance ? copy.placeCaption(nameOf(headlineBalance.id), period) : copy.movedCaption(period)}\n                </p>\n              </div>\n              <p className={readout ? \"min-h-10 shrink-0 text-sm\" : \"min-h-10 shrink-0 text-sm text-muted-foreground\"}>{readout ?? (touch ? copy.hintTouch : copy.hint)}</p>\n              <div className=\"flex shrink-0 flex-wrap items-center justify-between gap-x-2 gap-y-1 border-t pt-3\">\n                <p className=\"text-sm font-medium\">{copy.list}</p>\n                <div className=\"flex items-center gap-1\">\n                  {/* Always there, hidden and out of reach until a pick, so the row never grows when one lands. */}\n                  <Button\n                    type=\"button\"\n                    variant=\"ghost\"\n                    size=\"sm\"\n                    className={selected ? undefined : \"invisible\"}\n                    aria-hidden={selected ? undefined : true}\n                    inert={!selected}\n                    onClick={() => pick(null)}\n                  >\n                    {copy.showAll}\n                  </Button>\n                  <Button\n                    type=\"button\"\n                    variant=\"ghost\"\n                    size=\"sm\"\n                    className=\"sm:hidden\"\n                    aria-expanded={expanded}\n                    aria-controls={listId}\n                    onClick={() => setExpanded((open) => !open)}\n                  >\n                    {expanded ? copy.hideList : copy.showList}\n                  </Button>\n                </div>\n                {/* Gain and loss differ by side and sign as well as by colour. */}\n                <p className=\"flex w-full items-center gap-4 text-xs text-muted-foreground\">\n                  <span className=\"flex items-center gap-1.5\">\n                    <span aria-hidden=\"true\" className=\"h-2 w-3 rounded-sm bg-muted-foreground\" />\n                    {`− ${copy.lost}`}\n                  </span>\n                  <span className=\"flex items-center gap-1.5\">\n                    <span aria-hidden=\"true\" className=\"h-2 w-3 rounded-sm bg-primary\" />\n                    {`+ ${copy.gained}`}\n                  </span>\n                </p>\n              </div>\n              <div ref={scrollRef} data-flow-map-scroll=\"\" className=\"relative sm:-mx-2 sm:min-h-0 sm:flex-1 sm:overflow-y-auto sm:overscroll-contain sm:px-2\">\n                <ul\n                  id={listId}\n                  data-expanded={expanded}\n                  aria-label={copy.listLabel(period)}\n                  className=\"flex flex-col gap-0.5 max-sm:data-[expanded=false]:hidden\"\n                >\n                  {balances.map((balance) => {\n                    const picked = balance.id === selected;\n                    const gain = balance.net >= 0;\n                    return (\n                      <li key={balance.id}>\n                        <Button\n                          type=\"button\"\n                          variant={picked ? \"secondary\" : \"ghost\"}\n                          aria-pressed={picked}\n                          data-province={balance.id}\n                          data-hovered={hoveredRow === balance.id ? \"true\" : undefined}\n                          className=\"h-auto w-full flex-col items-stretch gap-1 py-1.5 text-left whitespace-normal transition-colors hover:bg-muted focus-visible:bg-muted data-[hovered=true]:bg-muted\"\n                          onClick={() => pick(balance.id)}\n                          onMouseEnter={() => enterRow(balance.id)}\n                          onMouseLeave={leaveRow}\n                          onFocus={() => enterRow(balance.id)}\n                          onBlur={leaveRow}\n                        >\n                          <span className=\"flex items-baseline justify-between gap-3\">\n                            <span className={picked ? \"flex min-w-0 items-center gap-1.5 font-semibold\" : \"flex min-w-0 items-center gap-1.5 font-medium\"}>\n                              {/* Picked is shown by weight and a mark, not by colour alone. */}\n                              {picked ? (\n                                <span aria-hidden=\"true\" data-selected-mark=\"\">\n                                  ✓\n                                </span>\n                              ) : null}\n                              {nameOf(balance.id)}\n                            </span>{\" \"}\n                            <span className=\"shrink-0 text-xs tabular-nums\">{formatNet(balance.net, lang)}</span>\n                          </span>\n                          {/* One scale for every year: the bar grows right of the axis for a gain, left for a loss. */}\n                          <span aria-hidden=\"true\" className=\"relative h-2 w-full\">\n                            <span className=\"absolute inset-y-0 left-1/2 w-px bg-border\" />\n                            <span\n                              data-net-bar={gain ? \"gain\" : \"loss\"}\n                              style={{ width: `${(Math.abs(balance.net) / LARGEST_NET) * 50}%` }}\n                              className={[\n                                \"absolute inset-y-0 rounded-sm motion-safe:transition-[width] motion-safe:duration-500 motion-safe:ease-out\",\n                                gain ? \"left-1/2 bg-primary\" : \"right-1/2 bg-muted-foreground\",\n                              ].join(\" \")}\n                            />\n                          </span>\n                        </Button>\n                      </li>\n                    );\n                  })}\n                </ul>\n                <p className=\"py-3 text-xs text-muted-foreground\">\n                  <a\n                    className=\"underline underline-offset-2\"\n                    href={lang === \"fr\" ? MIGRATION_SOURCE.urlFr : MIGRATION_SOURCE.url}\n                    target=\"_blank\"\n                    rel=\"noreferrer\"\n                  >\n                    {MIGRATION_SOURCE[lang]}\n                  </a>{\" \"}\n                  {copy.licences}{\" \"}\n                  <a className=\"underline underline-offset-2\" href={FLOW_MAP_LICENCES.statcan[lang]} target=\"_blank\" rel=\"noreferrer\">\n                    {copy.statcanLicence}\n                  </a>\n                  ,{\" \"}\n                  <a className=\"underline underline-offset-2\" href={FLOW_MAP_LICENCES.ogl[lang]} target=\"_blank\" rel=\"noreferrer\">\n                    {copy.oglLicence}\n                  </a>\n                  .\n                </p>\n              </div>\n            </div>\n          </CardContent>\n        </Card>\n      </div>\n    </>\n  );\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/lib/flow-map-data.ts",
      "content": "/**\n * Interprovincial migration: the sample data of the flow-map block.\n *\n * Adapted from Statistics Canada, Table 17-10-0022-01 Estimates of interprovincial migrants by\n * province or territory of origin and destination, annual (DOI 10.25318/1710002201-eng), the\n * five periods 2021/2022 to 2025/2026 (each 1 July to 30 June), retrieved 2026-09-29 from\n * https://www150.statcan.gc.ca/n1/tbl/csv/17100022-eng.zip. 2025/2026 is preliminary. Zero\n * flows are left out. Statistics Canada Open Licence.\n *\n * Locations are each province's or territory's capital, from the Canadian Geographical Names\n * Database (Natural Resources Canada), Open Government Licence, Canada.\n */\n\nexport interface MigrationLocation {\n  /** Postal abbreviation. */\n  id: string;\n  name: string;\n  name_fr: string;\n  capital: string;\n  lat: number;\n  lon: number;\n}\n\n/** [origin id, destination id, persons]. */\nexport type MigrationRow = [string, string, number];\n\nexport const MIGRATION_LOCATIONS: MigrationLocation[] = [\n  {\"id\":\"NL\",\"name\":\"Newfoundland and Labrador\",\"name_fr\":\"Terre-Neuve-et-Labrador\",\"capital\":\"St. John's\",\"lat\":47.4822,\"lon\":-52.79707},\n  {\"id\":\"PE\",\"name\":\"Prince Edward Island\",\"name_fr\":\"Île-du-Prince-Édouard\",\"capital\":\"Charlottetown\",\"lat\":46.24028,\"lon\":-63.13472},\n  {\"id\":\"NS\",\"name\":\"Nova Scotia\",\"name_fr\":\"Nouvelle-Écosse\",\"capital\":\"Halifax\",\"lat\":44.6474,\"lon\":-63.59065},\n  {\"id\":\"NB\",\"name\":\"New Brunswick\",\"name_fr\":\"Nouveau-Brunswick\",\"capital\":\"Fredericton\",\"lat\":45.94528,\"lon\":-66.66667},\n  {\"id\":\"QC\",\"name\":\"Quebec\",\"name_fr\":\"Québec\",\"capital\":\"Québec\",\"lat\":46.81382,\"lon\":-71.208},\n  {\"id\":\"ON\",\"name\":\"Ontario\",\"name_fr\":\"Ontario\",\"capital\":\"Toronto\",\"lat\":43.7417,\"lon\":-79.3733},\n  {\"id\":\"MB\",\"name\":\"Manitoba\",\"name_fr\":\"Manitoba\",\"capital\":\"Winnipeg\",\"lat\":49.88444,\"lon\":-97.14639},\n  {\"id\":\"SK\",\"name\":\"Saskatchewan\",\"name_fr\":\"Saskatchewan\",\"capital\":\"Regina\",\"lat\":50.45484,\"lon\":-104.60661},\n  {\"id\":\"AB\",\"name\":\"Alberta\",\"name_fr\":\"Alberta\",\"capital\":\"Edmonton\",\"lat\":53.53444,\"lon\":-113.49028},\n  {\"id\":\"BC\",\"name\":\"British Columbia\",\"name_fr\":\"Colombie-Britannique\",\"capital\":\"Victoria\",\"lat\":48.42833,\"lon\":-123.36472},\n  {\"id\":\"YT\",\"name\":\"Yukon\",\"name_fr\":\"Yukon\",\"capital\":\"Whitehorse\",\"lat\":60.72421,\"lon\":-135.0561},\n  {\"id\":\"NT\",\"name\":\"Northwest Territories\",\"name_fr\":\"Territoires du Nord-Ouest\",\"capital\":\"Yellowknife\",\"lat\":62.4536,\"lon\":-114.37},\n  {\"id\":\"NU\",\"name\":\"Nunavut\",\"name_fr\":\"Nunavut\",\"capital\":\"Iqaluit\",\"lat\":63.75,\"lon\":-68.51667},\n];\n\n/** Reference periods, oldest first. */\nexport const MIGRATION_PERIODS: string[] = [\"2021/2022\",\"2022/2023\",\"2023/2024\",\"2024/2025\",\"2025/2026\"];\n\n/** Statistics Canada marks this period's estimates as preliminary. */\nexport const MIGRATION_PRELIMINARY = \"2025/2026\";\n\nexport const MIGRATION: Record<string, MigrationRow[]> = {\n  \"2021/2022\": [[\"NL\",\"PE\",147],[\"NL\",\"NS\",999],[\"NL\",\"NB\",369],[\"NL\",\"QC\",243],[\"NL\",\"ON\",1902],[\"NL\",\"MB\",80],[\"NL\",\"SK\",83],[\"NL\",\"AB\",1668],[\"NL\",\"BC\",569],[\"NL\",\"YT\",10],[\"NL\",\"NT\",48],[\"NL\",\"NU\",40],[\"PE\",\"NL\",82],[\"PE\",\"NS\",546],[\"PE\",\"NB\",341],[\"PE\",\"QC\",150],[\"PE\",\"ON\",1559],[\"PE\",\"MB\",22],[\"PE\",\"SK\",21],[\"PE\",\"AB\",407],[\"PE\",\"BC\",382],[\"PE\",\"YT\",4],[\"PE\",\"NT\",11],[\"PE\",\"NU\",7],[\"NS\",\"NL\",875],[\"NS\",\"PE\",636],[\"NS\",\"NB\",1997],[\"NS\",\"QC\",838],[\"NS\",\"ON\",5206],[\"NS\",\"MB\",270],[\"NS\",\"SK\",206],[\"NS\",\"AB\",2069],[\"NS\",\"BC\",1486],[\"NS\",\"YT\",59],[\"NS\",\"NT\",92],[\"NS\",\"NU\",72],[\"NB\",\"NL\",334],[\"NB\",\"PE\",405],[\"NB\",\"NS\",2178],[\"NB\",\"QC\",1706],[\"NB\",\"ON\",3111],[\"NB\",\"MB\",198],[\"NB\",\"SK\",126],[\"NB\",\"AB\",1432],[\"NB\",\"BC\",801],[\"NB\",\"YT\",12],[\"NB\",\"NT\",47],[\"NB\",\"NU\",29],[\"QC\",\"NL\",189],[\"QC\",\"PE\",379],[\"QC\",\"NS\",1499],[\"QC\",\"NB\",2015],[\"QC\",\"ON\",21510],[\"QC\",\"MB\",1278],[\"QC\",\"SK\",457],[\"QC\",\"AB\",3111],[\"QC\",\"BC\",4819],[\"QC\",\"YT\",99],[\"QC\",\"NT\",74],[\"QC\",\"NU\",86],[\"ON\",\"NL\",3810],[\"ON\",\"PE\",2587],[\"ON\",\"NS\",13341],[\"ON\",\"NB\",9895],[\"ON\",\"QC\",18726],[\"ON\",\"MB\",6762],[\"ON\",\"SK\",3321],[\"ON\",\"AB\",23497],[\"ON\",\"BC\",25346],[\"ON\",\"YT\",400],[\"ON\",\"NT\",425],[\"ON\",\"NU\",298],[\"MB\",\"NL\",122],[\"MB\",\"PE\",67],[\"MB\",\"NS\",501],[\"MB\",\"NB\",305],[\"MB\",\"QC\",887],[\"MB\",\"ON\",7155],[\"MB\",\"SK\",1662],[\"MB\",\"AB\",6092],[\"MB\",\"BC\",5732],[\"MB\",\"YT\",31],[\"MB\",\"NT\",46],[\"MB\",\"NU\",57],[\"SK\",\"NL\",133],[\"SK\",\"PE\",51],[\"SK\",\"NS\",405],[\"SK\",\"NB\",196],[\"SK\",\"QC\",469],[\"SK\",\"ON\",5517],[\"SK\",\"MB\",1355],[\"SK\",\"AB\",10348],[\"SK\",\"BC\",5481],[\"SK\",\"YT\",70],[\"SK\",\"NT\",62],[\"SK\",\"NU\",29],[\"AB\",\"NL\",1709],[\"AB\",\"PE\",533],[\"AB\",\"NS\",3279],[\"AB\",\"NB\",1972],[\"AB\",\"QC\",2887],[\"AB\",\"ON\",16432],[\"AB\",\"MB\",2363],[\"AB\",\"SK\",5787],[\"AB\",\"BC\",32043],[\"AB\",\"YT\",342],[\"AB\",\"NT\",594],[\"AB\",\"NU\",114],[\"BC\",\"NL\",542],[\"BC\",\"PE\",367],[\"BC\",\"NS\",2478],[\"BC\",\"NB\",1087],[\"BC\",\"QC\",2822],[\"BC\",\"ON\",14190],[\"BC\",\"MB\",2725],[\"BC\",\"SK\",2802],[\"BC\",\"AB\",23742],[\"BC\",\"YT\",466],[\"BC\",\"NT\",262],[\"BC\",\"NU\",74],[\"YT\",\"NL\",15],[\"YT\",\"PE\",15],[\"YT\",\"NS\",42],[\"YT\",\"NB\",19],[\"YT\",\"QC\",66],[\"YT\",\"ON\",203],[\"YT\",\"MB\",36],[\"YT\",\"SK\",62],[\"YT\",\"AB\",312],[\"YT\",\"BC\",564],[\"YT\",\"NT\",37],[\"YT\",\"NU\",5],[\"NT\",\"NL\",89],[\"NT\",\"PE\",29],[\"NT\",\"NS\",122],[\"NT\",\"NB\",56],[\"NT\",\"QC\",78],[\"NT\",\"ON\",284],[\"NT\",\"MB\",64],[\"NT\",\"SK\",74],[\"NT\",\"AB\",917],[\"NT\",\"BC\",347],[\"NT\",\"YT\",122],[\"NT\",\"NU\",22],[\"NU\",\"NL\",60],[\"NU\",\"PE\",24],[\"NU\",\"NS\",117],[\"NU\",\"NB\",24],[\"NU\",\"QC\",115],[\"NU\",\"ON\",321],[\"NU\",\"MB\",53],[\"NU\",\"SK\",22],[\"NU\",\"AB\",106],[\"NU\",\"BC\",56],[\"NU\",\"YT\",48],[\"NU\",\"NT\",67]],\n  \"2022/2023\": [[\"NL\",\"PE\",120],[\"NL\",\"NS\",899],[\"NL\",\"NB\",391],[\"NL\",\"QC\",261],[\"NL\",\"ON\",1960],[\"NL\",\"MB\",118],[\"NL\",\"SK\",104],[\"NL\",\"AB\",1918],[\"NL\",\"BC\",568],[\"NL\",\"YT\",21],[\"NL\",\"NT\",53],[\"NL\",\"NU\",40],[\"PE\",\"NL\",76],[\"PE\",\"NS\",473],[\"PE\",\"NB\",362],[\"PE\",\"QC\",154],[\"PE\",\"ON\",1714],[\"PE\",\"MB\",26],[\"PE\",\"SK\",31],[\"PE\",\"AB\",613],[\"PE\",\"BC\",357],[\"PE\",\"YT\",2],[\"PE\",\"NT\",19],[\"PE\",\"NU\",4],[\"NS\",\"NL\",847],[\"NS\",\"PE\",505],[\"NS\",\"NB\",1912],[\"NS\",\"QC\",863],[\"NS\",\"ON\",6478],[\"NS\",\"MB\",236],[\"NS\",\"SK\",243],[\"NS\",\"AB\",2885],[\"NS\",\"BC\",1644],[\"NS\",\"YT\",46],[\"NS\",\"NT\",100],[\"NS\",\"NU\",48],[\"NB\",\"NL\",374],[\"NB\",\"PE\",396],[\"NB\",\"NS\",2028],[\"NB\",\"QC\",1835],[\"NB\",\"ON\",3568],[\"NB\",\"MB\",160],[\"NB\",\"SK\",142],[\"NB\",\"AB\",1825],[\"NB\",\"BC\",730],[\"NB\",\"YT\",24],[\"NB\",\"NT\",39],[\"NB\",\"NU\",27],[\"QC\",\"NL\",245],[\"QC\",\"PE\",834],[\"QC\",\"NS\",1615],[\"QC\",\"NB\",1883],[\"QC\",\"ON\",21032],[\"QC\",\"MB\",1383],[\"QC\",\"SK\",650],[\"QC\",\"AB\",4208],[\"QC\",\"BC\",4483],[\"QC\",\"YT\",98],[\"QC\",\"NT\",109],[\"QC\",\"NU\",87],[\"ON\",\"NL\",3491],[\"ON\",\"PE\",2595],[\"ON\",\"NS\",11788],[\"ON\",\"NB\",8685],[\"ON\",\"QC\",16273],[\"ON\",\"MB\",5843],[\"ON\",\"SK\",3831],[\"ON\",\"AB\",31484],[\"ON\",\"BC\",21301],[\"ON\",\"YT\",404],[\"ON\",\"NT\",400],[\"ON\",\"NU\",243],[\"MB\",\"NL\",116],[\"MB\",\"PE\",75],[\"MB\",\"NS\",434],[\"MB\",\"NB\",222],[\"MB\",\"QC\",853],[\"MB\",\"ON\",7520],[\"MB\",\"SK\",1567],[\"MB\",\"AB\",7230],[\"MB\",\"BC\",5407],[\"MB\",\"YT\",44],[\"MB\",\"NT\",53],[\"MB\",\"NU\",36],[\"SK\",\"NL\",90],[\"SK\",\"PE\",56],[\"SK\",\"NS\",309],[\"SK\",\"NB\",190],[\"SK\",\"QC\",364],[\"SK\",\"ON\",4899],[\"SK\",\"MB\",1231],[\"SK\",\"AB\",9784],[\"SK\",\"BC\",4656],[\"SK\",\"YT\",50],[\"SK\",\"NT\",50],[\"SK\",\"NU\",25],[\"AB\",\"NL\",1597],[\"AB\",\"PE\",338],[\"AB\",\"NS\",2438],[\"AB\",\"NB\",1409],[\"AB\",\"QC\",2454],[\"AB\",\"ON\",13615],[\"AB\",\"MB\",2114],[\"AB\",\"SK\",5348],[\"AB\",\"BC\",23015],[\"AB\",\"YT\",321],[\"AB\",\"NT\",436],[\"AB\",\"NU\",111],[\"BC\",\"NL\",522],[\"BC\",\"PE\",352],[\"BC\",\"NS\",2520],[\"BC\",\"NB\",1034],[\"BC\",\"QC\",2991],[\"BC\",\"ON\",14997],[\"BC\",\"MB\",2346],[\"BC\",\"SK\",2965],[\"BC\",\"AB\",31176],[\"BC\",\"YT\",535],[\"BC\",\"NT\",225],[\"BC\",\"NU\",36],[\"YT\",\"NL\",20],[\"YT\",\"PE\",4],[\"YT\",\"NS\",81],[\"YT\",\"NB\",14],[\"YT\",\"QC\",60],[\"YT\",\"ON\",207],[\"YT\",\"MB\",20],[\"YT\",\"SK\",59],[\"YT\",\"AB\",426],[\"YT\",\"BC\",489],[\"YT\",\"NT\",51],[\"YT\",\"NU\",4],[\"NT\",\"NL\",93],[\"NT\",\"PE\",17],[\"NT\",\"NS\",102],[\"NT\",\"NB\",46],[\"NT\",\"QC\",81],[\"NT\",\"ON\",275],[\"NT\",\"MB\",61],[\"NT\",\"SK\",82],[\"NT\",\"AB\",933],[\"NT\",\"BC\",367],[\"NT\",\"YT\",112],[\"NT\",\"NU\",34],[\"NU\",\"NL\",42],[\"NU\",\"PE\",10],[\"NU\",\"NS\",109],[\"NU\",\"NB\",51],[\"NU\",\"QC\",105],[\"NU\",\"ON\",343],[\"NU\",\"MB\",91],[\"NU\",\"SK\",26],[\"NU\",\"AB\",122],[\"NU\",\"BC\",70],[\"NU\",\"YT\",28],[\"NU\",\"NT\",84]],\n  \"2023/2024\": [[\"NL\",\"PE\",86],[\"NL\",\"NS\",872],[\"NL\",\"NB\",384],[\"NL\",\"QC\",236],[\"NL\",\"ON\",1914],[\"NL\",\"MB\",103],[\"NL\",\"SK\",114],[\"NL\",\"AB\",1857],[\"NL\",\"BC\",496],[\"NL\",\"YT\",15],[\"NL\",\"NT\",45],[\"NL\",\"NU\",39],[\"PE\",\"NL\",81],[\"PE\",\"NS\",570],[\"PE\",\"NB\",378],[\"PE\",\"QC\",220],[\"PE\",\"ON\",1384],[\"PE\",\"MB\",49],[\"PE\",\"SK\",29],[\"PE\",\"AB\",682],[\"PE\",\"BC\",328],[\"PE\",\"YT\",7],[\"PE\",\"NT\",15],[\"PE\",\"NU\",11],[\"NS\",\"NL\",705],[\"NS\",\"PE\",463],[\"NS\",\"NB\",1922],[\"NS\",\"QC\",1070],[\"NS\",\"ON\",5682],[\"NS\",\"MB\",277],[\"NS\",\"SK\",215],[\"NS\",\"AB\",3024],[\"NS\",\"BC\",1607],[\"NS\",\"YT\",28],[\"NS\",\"NT\",100],[\"NS\",\"NU\",47],[\"NB\",\"NL\",312],[\"NB\",\"PE\",307],[\"NB\",\"NS\",1797],[\"NB\",\"QC\",2234],[\"NB\",\"ON\",3432],[\"NB\",\"MB\",116],[\"NB\",\"SK\",136],[\"NB\",\"AB\",1906],[\"NB\",\"BC\",712],[\"NB\",\"YT\",10],[\"NB\",\"NT\",15],[\"NB\",\"NU\",26],[\"QC\",\"NL\",221],[\"QC\",\"PE\",502],[\"QC\",\"NS\",1714],[\"QC\",\"NB\",1890],[\"QC\",\"ON\",18577],[\"QC\",\"MB\",726],[\"QC\",\"SK\",705],[\"QC\",\"AB\",4141],[\"QC\",\"BC\",3874],[\"QC\",\"YT\",143],[\"QC\",\"NT\",89],[\"QC\",\"NU\",83],[\"ON\",\"NL\",3550],[\"ON\",\"PE\",2408],[\"ON\",\"NS\",13316],[\"ON\",\"NB\",8915],[\"ON\",\"QC\",15437],[\"ON\",\"MB\",5108],[\"ON\",\"SK\",6178],[\"ON\",\"AB\",35823],[\"ON\",\"BC\",19787],[\"ON\",\"YT\",515],[\"ON\",\"NT\",468],[\"ON\",\"NU\",314],[\"MB\",\"NL\",171],[\"MB\",\"PE\",87],[\"MB\",\"NS\",547],[\"MB\",\"NB\",289],[\"MB\",\"QC\",902],[\"MB\",\"ON\",5877],[\"MB\",\"SK\",1565],[\"MB\",\"AB\",6759],[\"MB\",\"BC\",4271],[\"MB\",\"YT\",49],[\"MB\",\"NT\",66],[\"MB\",\"NU\",60],[\"SK\",\"NL\",99],[\"SK\",\"PE\",56],[\"SK\",\"NS\",284],[\"SK\",\"NB\",187],[\"SK\",\"QC\",378],[\"SK\",\"ON\",4540],[\"SK\",\"MB\",1301],[\"SK\",\"AB\",9386],[\"SK\",\"BC\",4017],[\"SK\",\"YT\",56],[\"SK\",\"NT\",69],[\"SK\",\"NU\",24],[\"AB\",\"NL\",1380],[\"AB\",\"PE\",385],[\"AB\",\"NS\",2177],[\"AB\",\"NB\",1189],[\"AB\",\"QC\",2368],[\"AB\",\"ON\",12301],[\"AB\",\"MB\",2137],[\"AB\",\"SK\",4879],[\"AB\",\"BC\",19250],[\"AB\",\"YT\",272],[\"AB\",\"NT\",440],[\"AB\",\"NU\",110],[\"BC\",\"NL\",591],[\"BC\",\"PE\",405],[\"BC\",\"NS\",2516],[\"BC\",\"NB\",1146],[\"BC\",\"QC\",2797],[\"BC\",\"ON\",14644],[\"BC\",\"MB\",2214],[\"BC\",\"SK\",3396],[\"BC\",\"AB\",30502],[\"BC\",\"YT\",669],[\"BC\",\"NT\",364],[\"BC\",\"NU\",48],[\"YT\",\"NL\",16],[\"YT\",\"PE\",6],[\"YT\",\"NS\",40],[\"YT\",\"NB\",31],[\"YT\",\"QC\",56],[\"YT\",\"ON\",189],[\"YT\",\"MB\",42],[\"YT\",\"SK\",49],[\"YT\",\"AB\",292],[\"YT\",\"BC\",449],[\"YT\",\"NT\",44],[\"YT\",\"NU\",7],[\"NT\",\"NL\",54],[\"NT\",\"PE\",21],[\"NT\",\"NS\",89],[\"NT\",\"NB\",56],[\"NT\",\"QC\",74],[\"NT\",\"ON\",294],[\"NT\",\"MB\",68],[\"NT\",\"SK\",64],[\"NT\",\"AB\",814],[\"NT\",\"BC\",301],[\"NT\",\"YT\",97],[\"NT\",\"NU\",52],[\"NU\",\"NL\",72],[\"NU\",\"PE\",13],[\"NU\",\"NS\",59],[\"NU\",\"NB\",35],[\"NU\",\"QC\",143],[\"NU\",\"ON\",296],[\"NU\",\"MB\",103],[\"NU\",\"SK\",14],[\"NU\",\"AB\",96],[\"NU\",\"BC\",73],[\"NU\",\"YT\",32],[\"NU\",\"NT\",48]],\n  \"2024/2025\": [[\"NL\",\"PE\",90],[\"NL\",\"NS\",674],[\"NL\",\"NB\",316],[\"NL\",\"QC\",287],[\"NL\",\"ON\",1640],[\"NL\",\"MB\",94],[\"NL\",\"SK\",111],[\"NL\",\"AB\",1374],[\"NL\",\"BC\",462],[\"NL\",\"YT\",4],[\"NL\",\"NT\",24],[\"NL\",\"NU\",25],[\"PE\",\"NL\",55],[\"PE\",\"NS\",562],[\"PE\",\"NB\",350],[\"PE\",\"QC\",250],[\"PE\",\"ON\",1045],[\"PE\",\"MB\",44],[\"PE\",\"SK\",55],[\"PE\",\"AB\",538],[\"PE\",\"BC\",273],[\"PE\",\"YT\",9],[\"PE\",\"NT\",6],[\"PE\",\"NU\",16],[\"NS\",\"NL\",613],[\"NS\",\"PE\",412],[\"NS\",\"NB\",1728],[\"NS\",\"QC\",949],[\"NS\",\"ON\",5925],[\"NS\",\"MB\",293],[\"NS\",\"SK\",187],[\"NS\",\"AB\",2405],[\"NS\",\"BC\",1502],[\"NS\",\"YT\",34],[\"NS\",\"NT\",78],[\"NS\",\"NU\",21],[\"NB\",\"NL\",235],[\"NB\",\"PE\",311],[\"NB\",\"NS\",1881],[\"NB\",\"QC\",2081],[\"NB\",\"ON\",3333],[\"NB\",\"MB\",184],[\"NB\",\"SK\",133],[\"NB\",\"AB\",1637],[\"NB\",\"BC\",785],[\"NB\",\"YT\",12],[\"NB\",\"NT\",15],[\"NB\",\"NU\",16],[\"QC\",\"NL\",296],[\"QC\",\"PE\",186],[\"QC\",\"NS\",1329],[\"QC\",\"NB\",2217],[\"QC\",\"ON\",19953],[\"QC\",\"MB\",515],[\"QC\",\"SK\",496],[\"QC\",\"AB\",3718],[\"QC\",\"BC\",3712],[\"QC\",\"YT\",92],[\"QC\",\"NT\",79],[\"QC\",\"NU\",87],[\"ON\",\"NL\",3586],[\"ON\",\"PE\",1675],[\"ON\",\"NS\",11735],[\"ON\",\"NB\",8159],[\"ON\",\"QC\",14825],[\"ON\",\"MB\",5426],[\"ON\",\"SK\",4954],[\"ON\",\"AB\",27777],[\"ON\",\"BC\",20096],[\"ON\",\"YT\",437],[\"ON\",\"NT\",455],[\"ON\",\"NU\",238],[\"MB\",\"NL\",134],[\"MB\",\"PE\",73],[\"MB\",\"NS\",376],[\"MB\",\"NB\",268],[\"MB\",\"QC\",708],[\"MB\",\"ON\",4560],[\"MB\",\"SK\",1111],[\"MB\",\"AB\",4607],[\"MB\",\"BC\",3404],[\"MB\",\"YT\",44],[\"MB\",\"NT\",34],[\"MB\",\"NU\",34],[\"SK\",\"NL\",91],[\"SK\",\"PE\",43],[\"SK\",\"NS\",291],[\"SK\",\"NB\",189],[\"SK\",\"QC\",370],[\"SK\",\"ON\",4155],[\"SK\",\"MB\",1142],[\"SK\",\"AB\",7502],[\"SK\",\"BC\",3333],[\"SK\",\"YT\",39],[\"SK\",\"NT\",37],[\"SK\",\"NU\",25],[\"AB\",\"NL\",1209],[\"AB\",\"PE\",364],[\"AB\",\"NS\",2212],[\"AB\",\"NB\",1383],[\"AB\",\"QC\",2352],[\"AB\",\"ON\",13756],[\"AB\",\"MB\",2152],[\"AB\",\"SK\",4300],[\"AB\",\"BC\",19245],[\"AB\",\"YT\",268],[\"AB\",\"NT\",423],[\"AB\",\"NU\",95],[\"BC\",\"NL\",648],[\"BC\",\"PE\",269],[\"BC\",\"NS\",2270],[\"BC\",\"NB\",1067],[\"BC\",\"QC\",2645],[\"BC\",\"ON\",14677],[\"BC\",\"MB\",2172],[\"BC\",\"SK\",2931],[\"BC\",\"AB\",23456],[\"BC\",\"YT\",482],[\"BC\",\"NT\",298],[\"BC\",\"NU\",24],[\"YT\",\"NL\",14],[\"YT\",\"PE\",9],[\"YT\",\"NS\",40],[\"YT\",\"NB\",20],[\"YT\",\"QC\",32],[\"YT\",\"ON\",162],[\"YT\",\"MB\",27],[\"YT\",\"SK\",36],[\"YT\",\"AB\",295],[\"YT\",\"BC\",445],[\"YT\",\"NT\",38],[\"YT\",\"NU\",3],[\"NT\",\"NL\",57],[\"NT\",\"PE\",2],[\"NT\",\"NS\",64],[\"NT\",\"NB\",32],[\"NT\",\"QC\",62],[\"NT\",\"ON\",221],[\"NT\",\"MB\",35],[\"NT\",\"SK\",49],[\"NT\",\"AB\",741],[\"NT\",\"BC\",242],[\"NT\",\"YT\",81],[\"NT\",\"NU\",10],[\"NU\",\"NL\",44],[\"NU\",\"PE\",11],[\"NU\",\"NS\",28],[\"NU\",\"NB\",15],[\"NU\",\"QC\",61],[\"NU\",\"ON\",223],[\"NU\",\"MB\",94],[\"NU\",\"SK\",10],[\"NU\",\"AB\",85],[\"NU\",\"BC\",19],[\"NU\",\"YT\",16],[\"NU\",\"NT\",33]],\n  \"2025/2026\": [[\"NL\",\"PE\",54],[\"NL\",\"NS\",936],[\"NL\",\"NB\",470],[\"NL\",\"QC\",240],[\"NL\",\"ON\",1598],[\"NL\",\"MB\",125],[\"NL\",\"SK\",106],[\"NL\",\"AB\",1328],[\"NL\",\"BC\",747],[\"NL\",\"YT\",24],[\"NL\",\"NT\",48],[\"NL\",\"NU\",36],[\"PE\",\"NL\",91],[\"PE\",\"NS\",746],[\"PE\",\"NB\",396],[\"PE\",\"QC\",588],[\"PE\",\"ON\",1190],[\"PE\",\"MB\",82],[\"PE\",\"SK\",20],[\"PE\",\"AB\",529],[\"PE\",\"BC\",382],[\"PE\",\"YT\",5],[\"PE\",\"NT\",9],[\"NS\",\"NL\",623],[\"NS\",\"PE\",539],[\"NS\",\"NB\",1690],[\"NS\",\"QC\",735],[\"NS\",\"ON\",5702],[\"NS\",\"MB\",324],[\"NS\",\"SK\",347],[\"NS\",\"AB\",2241],[\"NS\",\"BC\",1380],[\"NS\",\"YT\",80],[\"NS\",\"NT\",35],[\"NS\",\"NU\",138],[\"NB\",\"NL\",338],[\"NB\",\"PE\",316],[\"NB\",\"NS\",2206],[\"NB\",\"QC\",2375],[\"NB\",\"ON\",3657],[\"NB\",\"MB\",156],[\"NB\",\"SK\",141],[\"NB\",\"AB\",1961],[\"NB\",\"BC\",966],[\"NB\",\"YT\",42],[\"NB\",\"NT\",12],[\"NB\",\"NU\",65],[\"QC\",\"NL\",266],[\"QC\",\"PE\",341],[\"QC\",\"NS\",1161],[\"QC\",\"NB\",2002],[\"QC\",\"ON\",18802],[\"QC\",\"MB\",860],[\"QC\",\"SK\",497],[\"QC\",\"AB\",3786],[\"QC\",\"BC\",3832],[\"QC\",\"YT\",177],[\"QC\",\"NT\",55],[\"QC\",\"NU\",111],[\"ON\",\"NL\",2932],[\"ON\",\"PE\",1649],[\"ON\",\"NS\",7802],[\"ON\",\"NB\",5070],[\"ON\",\"QC\",15127],[\"ON\",\"MB\",4925],[\"ON\",\"SK\",4681],[\"ON\",\"AB\",24344],[\"ON\",\"BC\",18632],[\"ON\",\"YT\",464],[\"ON\",\"NT\",397],[\"ON\",\"NU\",518],[\"MB\",\"NL\",298],[\"MB\",\"PE\",49],[\"MB\",\"NS\",517],[\"MB\",\"NB\",287],[\"MB\",\"QC\",704],[\"MB\",\"ON\",5434],[\"MB\",\"SK\",1821],[\"MB\",\"AB\",5598],[\"MB\",\"BC\",3261],[\"MB\",\"YT\",68],[\"MB\",\"NT\",57],[\"MB\",\"NU\",97],[\"SK\",\"NL\",171],[\"SK\",\"PE\",19],[\"SK\",\"NS\",328],[\"SK\",\"NB\",156],[\"SK\",\"QC\",387],[\"SK\",\"ON\",4135],[\"SK\",\"MB\",1523],[\"SK\",\"AB\",8965],[\"SK\",\"BC\",3323],[\"SK\",\"YT\",63],[\"SK\",\"NT\",55],[\"SK\",\"NU\",28],[\"AB\",\"NL\",1292],[\"AB\",\"PE\",388],[\"AB\",\"NS\",1814],[\"AB\",\"NB\",1226],[\"AB\",\"QC\",2130],[\"AB\",\"ON\",13449],[\"AB\",\"MB\",2541],[\"AB\",\"SK\",6044],[\"AB\",\"BC\",19762],[\"AB\",\"YT\",387],[\"AB\",\"NT\",617],[\"AB\",\"NU\",184],[\"BC\",\"NL\",482],[\"BC\",\"PE\",478],[\"BC\",\"NS\",1952],[\"BC\",\"NB\",785],[\"BC\",\"QC\",2256],[\"BC\",\"ON\",13646],[\"BC\",\"MB\",2219],[\"BC\",\"SK\",2503],[\"BC\",\"AB\",21409],[\"BC\",\"YT\",389],[\"BC\",\"NT\",160],[\"BC\",\"NU\",58],[\"YT\",\"NL\",43],[\"YT\",\"PE\",26],[\"YT\",\"NS\",54],[\"YT\",\"NB\",36],[\"YT\",\"QC\",85],[\"YT\",\"ON\",276],[\"YT\",\"MB\",36],[\"YT\",\"SK\",59],[\"YT\",\"AB\",186],[\"YT\",\"BC\",482],[\"YT\",\"NT\",72],[\"YT\",\"NU\",9],[\"NT\",\"NL\",62],[\"NT\",\"PE\",25],[\"NT\",\"NS\",143],[\"NT\",\"NB\",37],[\"NT\",\"QC\",91],[\"NT\",\"ON\",238],[\"NT\",\"MB\",16],[\"NT\",\"SK\",82],[\"NT\",\"AB\",1019],[\"NT\",\"BC\",455],[\"NT\",\"YT\",130],[\"NT\",\"NU\",81],[\"NU\",\"NL\",46],[\"NU\",\"PE\",13],[\"NU\",\"NS\",38],[\"NU\",\"NB\",63],[\"NU\",\"QC\",88],[\"NU\",\"ON\",399],[\"NU\",\"MB\",93],[\"NU\",\"SK\",15],[\"NU\",\"AB\",130],[\"NU\",\"BC\",43],[\"NU\",\"YT\",38],[\"NU\",\"NT\",61]],\n};\n\n/** Credit lines the block prints under the map. The licences ask for this wording. */\nexport const MIGRATION_SOURCE = {\n  en: \"Adapted from Statistics Canada, Table 17-10-0022-01 Estimates of interprovincial migrants by province or territory of origin and destination, annual, 2021/2022 to 2025/2026. This does not constitute an endorsement by Statistics Canada of this product. Capitals: Canadian Geographical Names Database. Contains information licensed under the Open Government Licence – Canada.\",\n  fr: \"Adapté de Statistique Canada, Tableau 17-10-0022-01 Estimations des migrants interprovinciaux selon la province ou territoire d'origine et de destination, annuelles, 2021/2022 à 2025/2026. Cela ne constitue pas une approbation de ce produit par Statistique Canada. Capitales : Base de données toponymiques du Canada. Contient de l'information visée par la Licence du gouvernement ouvert – Canada.\",\n  url: \"https://doi.org/10.25318/1710002201-eng\",\n  urlFr: \"https://doi.org/10.25318/1710002201-fra\",\n};\n",
      "type": "registry:lib"
    }
  ],
  "envVars": {
    "NEXT_PUBLIC_UNMAP_KEY": ""
  },
  "docs": "Paste your unmap key into NEXT_PUBLIC_UNMAP_KEY in .env.local, then open /flow-map. Guide: https://unmap.dev/docs/frontend/blocks#follow-interprovincial-migration",
  "categories": [
    "blocks"
  ],
  "type": "registry:block"
}