diff --git a/2024/README.md b/2024/README.md
index a2e05b2..b4cde7f 100644
--- a/2024/README.md
+++ b/2024/README.md
@@ -3,7 +3,7 @@
 |  S  |  M  |  T  |  W  |  T  |  F  |  S  |
 | :-: | :-: | :-: | :-: | :-: | :-: | :-: |
 | [1] | [2] | [3] | [4] | [5] | [6] | [7] |
-| [8] | [9] | [10]| [11]|  12 |  13 |  14 |
+| [8] | [9] | [10]| [11]| [12]|  13 |  14 |
 |  15 |  16 |  17 |  18 |  19 |  20 |  21 |
 |  22 |  23 |  24 |  25 |     |     |     |
 
@@ -18,3 +18,4 @@
 [9]: ./lib/2024/9.ex
 [10]: ./lib/2024/10.ex
 [11]: ./lib/2024/11.ex
+[12]: ./lib/2024/11.ex
diff --git a/2024/lib/2024/12.ex b/2024/lib/2024/12.ex
new file mode 100644
index 0000000..26167d0
--- /dev/null
+++ b/2024/lib/2024/12.ex
@@ -0,0 +1,112 @@
+import AOC
+import AOC.Prelude
+
+aoc 2024, 12 do
+  def p1(input) do
+    input
+    |> map_grid()
+    |> group_plots()
+    |> Enum.map(&size_plot/1)
+    |> Enum.map(fn {area, perimeter, _} -> area * perimeter end)
+    |> Enum.sum()
+  end
+
+  def p2(input) do
+    input
+    |> map_grid()
+    |> group_plots()
+    |> Enum.map(&size_plot/1)
+    |> Enum.map(fn {area, _, sides} -> area * sides end)
+    |> Enum.sum()
+  end
+
+  ## group plots
+
+  defp group_plots(map, plots \\ [])
+  defp group_plots(map, plots) when map_size(map) == 0, do: plots
+
+  defp group_plots(map, plots) do
+    {pos, plant} = Enum.at(map, 0)
+
+    plot = walk_plot(map, pos, plant)
+
+    {_, map} = Map.split(map, MapSet.to_list(plot))
+
+    group_plots(map, [plot | plots])
+  end
+
+  defp size_plot(plot) do
+    area = MapSet.size(plot)
+
+    perimeter =
+      for pos <- plot, reduce: 0 do
+        sum ->
+          sum +
+            ([&n/1, &s/1, &e/1, &w/1]
+             |> Enum.map(& &1.(pos))
+             |> Enum.count(&(not MapSet.member?(plot, &1))))
+      end
+
+    {area, perimeter, count_sides(plot)}
+  end
+
+  ## traversal
+
+  defp walk_plot(map, pos, plant, plot \\ MapSet.new()) do
+    plot = MapSet.put(plot, pos)
+
+    [&n/1, &s/1, &e/1, &w/1]
+    |> Enum.map(& &1.(pos))
+    |> Enum.reject(&MapSet.member?(plot, &1))
+    |> Enum.filter(&(Map.get(map, &1) == plant))
+    |> Enum.reduce(plot, fn pos, plot ->
+      walk_plot(map, pos, plant, plot)
+    end)
+  end
+
+  def n({x, y}), do: {x, y + 1}
+  def s({x, y}), do: {x, y - 1}
+  def e({x, y}), do: {x + 1, y}
+  def w({x, y}), do: {x - 1, y}
+
+  defp count_sides(plot) do
+    # find exterior faces
+    exterior_faces =
+      for pos <- plot, dir <- ~w[n e s w]a, exterior?(plot, pos, dir) do
+        {pos, dir}
+      end
+
+    exterior_faces
+    # group exterior faces
+    |> Enum.group_by(
+      fn
+        {{_, y}, dir} when dir in ~w[n s]a -> {dir, y}
+        {{x, _}, dir} when dir in ~w[e w]a -> {dir, x}
+      end,
+      fn
+        {{x, _}, dir} when dir in ~w[n s]a -> x
+        {{_, y}, dir} when dir in ~w[e w]a -> y
+      end
+    )
+    |> Map.values()
+    # count groups of exterior faces, splitting non-monotonic sections
+    |> Enum.map(fn group ->
+      {faces, _} =
+        group
+        |> Enum.sort()
+        |> Enum.reduce({0, nil}, fn
+          n, {sides, nil} -> {sides + 1, n}
+          n, {sides, last} when last + 1 == n -> {sides, n}
+          n, {sides, _last} -> {sides + 1, n}
+        end)
+
+      faces
+    end)
+    |> Enum.sum()
+  end
+
+  defp exterior?(plot, pos, dir) do
+    neighboring_pos = apply(__MODULE__, dir, [pos])
+    not MapSet.member?(plot, neighboring_pos)
+  end
+end
diff --git a/README.md b/README.md
index c783e5c..7bf0285 100644
--- a/README.md
+++ b/README.md
@@ -15,8 +15,8 @@
 | [2021] | **43/50** 🌟 | Elixir |
 | [2022] | **14/50** 🌟 | Elixir, Haskell |
 | [2023] | **19/50** 🌟 | Elixir, Haskell |
-| [2024] | **22/50** 🌟 | Elixir |
-| **Total** | **178** 🌟| |
+| [2024] | **24/50** 🌟 | Elixir |
+| **Total** | **180** 🌟| |
 
 [2015]: ./2015
 [2017]: ./2017