From 6ec259a0e71174651bae95d4628138bf6fd68742 Mon Sep 17 00:00:00 2001
From: kj_sh604
Date: Sun, 15 Mar 2026 16:19:35 -0400
Subject: refactor: packages/

---
 packages/excalidraw/actions/actionDistribute.tsx | 110 +++++++++++++++++++++++
 1 file changed, 110 insertions(+)
 create mode 100644 packages/excalidraw/actions/actionDistribute.tsx

(limited to 'packages/excalidraw/actions/actionDistribute.tsx')

diff --git a/packages/excalidraw/actions/actionDistribute.tsx b/packages/excalidraw/actions/actionDistribute.tsx
new file mode 100644
index 0000000..15d33b1
--- /dev/null
+++ b/packages/excalidraw/actions/actionDistribute.tsx
@@ -0,0 +1,110 @@
+import {
+  DistributeHorizontallyIcon,
+  DistributeVerticallyIcon,
+} from "../components/icons";
+import { ToolButton } from "../components/ToolButton";
+import type { Distribution } from "../distribute";
+import { distributeElements } from "../distribute";
+import { getNonDeletedElements } from "../element";
+import { isFrameLikeElement } from "../element/typeChecks";
+import type { ExcalidrawElement } from "../element/types";
+import { updateFrameMembershipOfSelectedElements } from "../frame";
+import { t } from "../i18n";
+import { CODES, KEYS } from "../keys";
+import { isSomeElementSelected } from "../scene";
+import { CaptureUpdateAction } from "../store";
+import type { AppClassProperties, AppState } from "../types";
+import { arrayToMap, getShortcutKey } from "../utils";
+import { register } from "./register";
+
+const enableActionGroup = (appState: AppState, app: AppClassProperties) => {
+  const selectedElements = app.scene.getSelectedElements(appState);
+  return (
+    selectedElements.length > 1 &&
+    // TODO enable distributing frames when implemented properly
+    !selectedElements.some((el) => isFrameLikeElement(el))
+  );
+};
+
+const distributeSelectedElements = (
+  elements: readonly ExcalidrawElement[],
+  appState: Readonly<AppState>,
+  app: AppClassProperties,
+  distribution: Distribution,
+) => {
+  const selectedElements = app.scene.getSelectedElements(appState);
+
+  const updatedElements = distributeElements(
+    selectedElements,
+    app.scene.getNonDeletedElementsMap(),
+    distribution,
+  );
+
+  const updatedElementsMap = arrayToMap(updatedElements);
+
+  return updateFrameMembershipOfSelectedElements(
+    elements.map((element) => updatedElementsMap.get(element.id) || element),
+    appState,
+    app,
+  );
+};
+
+export const distributeHorizontally = register({
+  name: "distributeHorizontally",
+  label: "labels.distributeHorizontally",
+  trackEvent: { category: "element" },
+  perform: (elements, appState, _, app) => {
+    return {
+      appState,
+      elements: distributeSelectedElements(elements, appState, app, {
+        space: "between",
+        axis: "x",
+      }),
+      captureUpdate: CaptureUpdateAction.IMMEDIATELY,
+    };
+  },
+  keyTest: (event) =>
+    !event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.H,
+  PanelComponent: ({ elements, appState, updateData, app }) => (
+    <ToolButton
+      hidden={!enableActionGroup(appState, app)}
+      type="button"
+      icon={DistributeHorizontallyIcon}
+      onClick={() => updateData(null)}
+      title={`${t("labels.distributeHorizontally")} — ${getShortcutKey(
+        "Alt+H",
+      )}`}
+      aria-label={t("labels.distributeHorizontally")}
+      visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
+    />
+  ),
+});
+
+export const distributeVertically = register({
+  name: "distributeVertically",
+  label: "labels.distributeVertically",
+  trackEvent: { category: "element" },
+  perform: (elements, appState, _, app) => {
+    return {
+      appState,
+      elements: distributeSelectedElements(elements, appState, app, {
+        space: "between",
+        axis: "y",
+      }),
+      captureUpdate: CaptureUpdateAction.IMMEDIATELY,
+    };
+  },
+  keyTest: (event) =>
+    !event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.V,
+  PanelComponent: ({ elements, appState, updateData, app }) => (
+    <ToolButton
+      hidden={!enableActionGroup(appState, app)}
+      type="button"
+      icon={DistributeVerticallyIcon}
+      onClick={() => updateData(null)}
+      title={`${t("labels.distributeVertically")} — ${getShortcutKey("Alt+V")}`}
+      aria-label={t("labels.distributeVertically")}
+      visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
+    />
+  ),
+});
-- 
cgit v1.2.3

