diff --git a/MigrationMap/MANIFEST b/MigrationMap/MANIFEST
new file mode 100644
index 000000000..38dee026f
--- /dev/null
+++ b/MigrationMap/MANIFEST
@@ -0,0 +1 @@
+MigrationMap/README.md
diff --git a/MigrationMap/MigrationMap.gpr.py b/MigrationMap/MigrationMap.gpr.py
new file mode 100644
index 000000000..b88847ae0
--- /dev/null
+++ b/MigrationMap/MigrationMap.gpr.py
@@ -0,0 +1,48 @@
+#
+# Gramps - a GTK+/GNOME based genealogy program
+#
+# Copyright (C) 2026 Brian Caudill
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, see .
+#
+
+# ------------------------------------------------------------------------
+#
+# Register the Migration Map tool
+#
+# ------------------------------------------------------------------------
+register(
+ TOOL,
+ id="MigrationMap",
+ name=_("Migration Map"),
+ description=_(
+ "Generate an animated, interactive map of how people and families "
+ "moved over time, built from dated events whose places have "
+ "coordinates. Opens in your web browser with a timeline you can play."
+ ),
+ version="0.0.1",
+ gramps_target_version="6.0",
+ status=STABLE,
+ audience=EXPERT,
+ fname="MigrationMap.py",
+ category=TOOL_ANAL,
+ toolclass="MigrationMapWindow",
+ optionclass="MigrationMapOptions",
+ tool_modes=[TOOL_MODE_GUI],
+ authors=["Brian Caudill"],
+ authors_email=["brian.m.caudill@gmail.com"],
+ maintainers=["Brian Caudill"],
+ maintainers_email=["brian.m.caudill@gmail.com"],
+ help_url="Addon:MigrationMap",
+)
diff --git a/MigrationMap/MigrationMap.py b/MigrationMap/MigrationMap.py
new file mode 100644
index 000000000..2fbab0dfd
--- /dev/null
+++ b/MigrationMap/MigrationMap.py
@@ -0,0 +1,419 @@
+#
+# Gramps - a GTK+/GNOME based genealogy program
+#
+# Copyright (C) 2026 Brian Caudill
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, see .
+#
+
+"""
+Migration Map tool.
+
+Collects every dated event whose place has coordinates, then writes a
+self-contained HTML page with an animated Leaflet map: press play and each
+person's moves draw themselves across the map as the years advance, with a
+pulsing frontier on the moves happening "now" and a running readout of who is
+moving where. Pairs with the Geocode Places tool, which fills in the
+coordinates this relies on.
+"""
+
+# -------------------------------------------------------------------------
+#
+# Standard Python modules
+#
+# -------------------------------------------------------------------------
+import os
+import html
+import json
+import pathlib
+import tempfile
+import webbrowser
+
+# -------------------------------------------------------------------------
+#
+# Gramps modules
+#
+# -------------------------------------------------------------------------
+from gramps.gen.display.place import displayer as place_displayer
+from gramps.gen.utils.place import conv_lat_lon
+from gramps.gen.plug.menu import BooleanOption, StringOption
+from gramps.gui.plug import MenuToolOptions, PluginWindows
+from gramps.gen.const import GRAMPS_LOCALE as glocale
+
+try:
+ _trans = glocale.get_addon_translator(__file__)
+except ValueError:
+ _trans = glocale.translation
+_ = _trans.gettext
+
+
+# -------------------------------------------------------------------------
+#
+# HTML template (Leaflet; data spliced in at the marked tokens)
+#
+# -------------------------------------------------------------------------
+HTML = r"""
+
+
+__TITLE__
+
+
+
+
+
__TITLE__
+
Press ▶ or drag the gold handle to watch each person's moves
+ draw themselves across the years. Pulsing dots are moves happening that year.
+
+
+
—
+
+
Drag the timeline or press play…
+
+
+
+
+
+
+ Drag the gold handle to scrub through time.
+
+
+
+
+
+
+"""
+
+
+# -------------------------------------------------------------------------
+#
+# MigrationMapOptions
+#
+# -------------------------------------------------------------------------
+class MigrationMapOptions(MenuToolOptions):
+ """
+ Options for the Migration Map tool.
+ """
+
+ def add_menu_options(self, menu):
+ """
+ Add the tool options.
+ """
+ category = _("Options")
+
+ draw_paths = BooleanOption(_("Draw migration paths"), True)
+ draw_paths.set_help(
+ _("Connect each person's locations in time order with a line that "
+ "grows as the animation plays.")
+ )
+ menu.add_option(category, "draw_paths", draw_paths)
+
+ output = StringOption(_("Output HTML file (blank = temp file)"), "")
+ output.set_help(_("Where to write the map; left blank uses a temp file."))
+ menu.add_option(category, "output", output)
+
+
+# -------------------------------------------------------------------------
+#
+# MigrationMapWindow
+#
+# -------------------------------------------------------------------------
+class MigrationMapWindow(PluginWindows.ToolManagedWindowBatch):
+ """
+ Tool window that builds and opens the animated migration map.
+ """
+
+ def get_title(self):
+ """
+ Return the tool window title.
+ """
+ return _("Migration Map")
+
+ def initial_frame(self):
+ """
+ Return the name of the options frame.
+ """
+ return _("Options")
+
+ def collect_events(self):
+ """
+ Return a list of dated, geocoded event dicts for every person.
+ """
+ events = []
+ self.progress.set_pass(
+ _("Collecting located events..."), self.db.get_number_of_people()
+ )
+ for person in self.db.iter_people():
+ self.progress.step()
+ name = person.get_primary_name().get_name()
+ surname = person.get_primary_name().get_surname()
+ pid = person.get_gramps_id()
+ for eref in person.get_event_ref_list():
+ event = self.db.get_event_from_handle(eref.ref)
+ if event is None:
+ continue
+ handle = event.get_place_handle()
+ if not handle:
+ continue
+ place = self.db.get_place_from_handle(handle)
+ if place is None:
+ continue
+ lat = (place.get_latitude() or "").strip()
+ lon = (place.get_longitude() or "").strip()
+ if not lat or not lon:
+ continue
+ dlat, dlon = conv_lat_lon(lat, lon, "D.D8")
+ if not dlat or not dlon:
+ continue
+ year = event.get_date_object().get_year()
+ if not year:
+ continue
+ # Escape free-text fields: rendered as HTML in the Leaflet popup.
+ events.append(
+ {
+ "p": pid,
+ "name": html.escape(name),
+ "s": html.escape(surname),
+ "year": year,
+ "lat": float(dlat),
+ "lon": float(dlon),
+ "place": html.escape(place_displayer.display(self.db, place)),
+ "etype": html.escape(str(event.get_type())),
+ }
+ )
+ return events
+
+ def run(self):
+ """
+ Build the animated map and open it in the browser.
+ """
+ opts = self.options.handler.options_dict
+ events = self.collect_events()
+
+ self.add_results_frame(_("Results"))
+ if not events:
+ self.results_write(
+ _(
+ "No dated events with coordinates were found. Add place "
+ "coordinates first (for example with the Geocode Places "
+ "tool), then run this again.\n"
+ )
+ )
+ return
+
+ title = html.escape(_("Migration Map - %s") % self.db.get_dbname())
+ page = (
+ HTML.replace("__DATA__", json.dumps(events))
+ .replace("__PATHS__", "true" if opts["draw_paths"] else "false")
+ .replace("__TITLE__", title)
+ )
+
+ output = (opts["output"] or "").strip()
+ if not output:
+ output = os.path.join(tempfile.gettempdir(), "gramps_migration_map.html")
+ with open(output, "w", encoding="utf-8") as handle:
+ handle.write(page)
+
+ years = [event["year"] for event in events]
+ people = len({event["p"] for event in events})
+ self.results_write(
+ _("Mapped %(events)d located events for %(people)d people, "
+ "%(start)d-%(end)d.\n")
+ % {"events": len(events), "people": people,
+ "start": min(years), "end": max(years)}
+ )
+ self.results_write(_("Opening: %s\n") % output)
+ webbrowser.open(pathlib.Path(output).as_uri())
diff --git a/MigrationMap/README.md b/MigrationMap/README.md
new file mode 100644
index 000000000..8581204b3
--- /dev/null
+++ b/MigrationMap/README.md
@@ -0,0 +1,40 @@
+# Migration Map
+
+A Gramps **Tool** that builds an animated, interactive map of how people and
+families moved over time, then opens it in your web browser.
+
+It reads every dated event whose place has coordinates, groups them per person
+in time order, and writes a self-contained HTML page (Leaflet) with a timeline
+you can play: each person's moves **draw themselves across the years**, dots
+**pulse** on the moves happening that year, and a readout shows who is moving
+where. Paths are colored by surname.
+
+## Usage
+
+**Tools → Analysis and Exploration → Migration Map**
+
+Options:
+
+| Option | Default | Meaning |
+|--------|---------|---------|
+| Draw migration paths | on | Connect each person's locations in time order with a line that grows as the animation plays. |
+| Output HTML file | (blank) | Where to write the page; blank uses a temp file. |
+
+Press **▶ Play**, drag the gold handle to scrub through time, or **⏮ Restart**.
+
+## Requirements
+
+The map only shows places that have **coordinates**. Genealogy imports usually
+have place names but no coordinates, so run a geocoder first — for example the
+companion **Geocode Places** tool, or set coordinates with the **Place
+Coordinate Gramplet**. Events also need a year.
+
+## Notes
+
+- The page loads Leaflet and OpenStreetMap tiles from the internet (with
+ Subresource Integrity pinned), so an internet connection is needed to view it.
+- All person, place, and event text is HTML-escaped before being embedded.
+
+## Contact
+
+Brian Caudill — brian.m.caudill@gmail.com
diff --git a/MigrationMap/po/template.pot b/MigrationMap/po/template.pot
new file mode 100644
index 000000000..1e588ace4
--- /dev/null
+++ b/MigrationMap/po/template.pot
@@ -0,0 +1,83 @@
+# Translations template for PROJECT.
+# Copyright (C) 2026 ORGANIZATION
+# This file is distributed under the same license as the PROJECT project.
+# FIRST AUTHOR , 2026.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PROJECT VERSION\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2026-06-01 16:03-0400\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 2.17.0\n"
+
+#: MigrationMap/MigrationMap.py:337
+msgid "Collecting located events..."
+msgstr ""
+
+#: MigrationMap/MigrationMap.py:299
+msgid ""
+"Connect each person's locations in time order with a line that grows as "
+"the animation plays."
+msgstr ""
+
+#: MigrationMap/MigrationMap.py:297
+msgid "Draw migration paths"
+msgstr ""
+
+#: MigrationMap/MigrationMap.gpr.py:30
+msgid ""
+"Generate an animated, interactive map of how people and families moved "
+"over time, built from dated events whose places have coordinates. Opens "
+"in your web browser with a timeline you can play."
+msgstr ""
+
+#: MigrationMap/MigrationMap.py:413
+#, python-format
+msgid ""
+"Mapped %(events)d located events for %(people)d people, "
+"%(start)d-%(end)d.\n"
+msgstr ""
+
+#: MigrationMap/MigrationMap.gpr.py:28 MigrationMap/MigrationMap.py:323
+msgid "Migration Map"
+msgstr ""
+
+#: MigrationMap/MigrationMap.py:397
+#, python-format
+msgid "Migration Map - %s"
+msgstr ""
+
+#: MigrationMap/MigrationMap.py:390
+msgid ""
+"No dated events with coordinates were found. Add place coordinates first "
+"(for example with the Geocode Places tool), then run this again.\n"
+msgstr ""
+
+#: MigrationMap/MigrationMap.py:418
+#, python-format
+msgid "Opening: %s\n"
+msgstr ""
+
+#: MigrationMap/MigrationMap.py:295 MigrationMap/MigrationMap.py:329
+msgid "Options"
+msgstr ""
+
+#: MigrationMap/MigrationMap.py:304
+msgid "Output HTML file (blank = temp file)"
+msgstr ""
+
+#: MigrationMap/MigrationMap.py:386
+msgid "Results"
+msgstr ""
+
+#: MigrationMap/MigrationMap.py:305
+msgid "Where to write the map; left blank uses a temp file."
+msgstr ""
+