- );
-}
-
// Figma-style ease-type dropdown: the current ease (glyph + name) as a button
// that opens the preset grid in a popover. This is where a preset is selected —
// the grid is no longer shown inline.
diff --git a/packages/studio/src/components/editor/EaseModeControls.tsx b/packages/studio/src/components/editor/EaseModeControls.tsx
new file mode 100644
index 000000000..40cac0ab3
--- /dev/null
+++ b/packages/studio/src/components/editor/EaseModeControls.tsx
@@ -0,0 +1,110 @@
+/**
+ * Ease editor mode primitives: the mode radio group and the preset grid.
+ *
+ * Split out of `EaseCurveSection.tsx` to keep it under the 600-line cap
+ * (CI's file size check). Both components are presentational and stateless —
+ * they take the current selection and emit a committed ease string — so they
+ * carry the mode vocabulary (`EASE_MODES`, labels, per-mode defaults) with
+ * them rather than importing it back from the parent.
+ */
+
+import { EASE_PRESETS } from "./easePresetLibrary";
+import { MiniCurveSvg } from "./easeCurveSvg";
+import { EASE_CURVES } from "./gsapAnimationConstants";
+
+const EASE_MODES = ["curve", "spring", "wiggle"] as const;
+export type EaseMode = (typeof EASE_MODES)[number];
+
+export type Pts = [number, number, number, number];
+export const DEFAULT_CURVE: Pts = EASE_CURVES["power2.out"];
+
+export const MODE_LABELS = { curve: "Curve", spring: "Spring", wiggle: "Wiggle" } satisfies Record<
+ EaseMode,
+ string
+>;
+
+const DEFAULT_EASE_BY_MODE = {
+ curve: `custom(M0,0 C${DEFAULT_CURVE[0]},${DEFAULT_CURVE[1]} ${DEFAULT_CURVE[2]},${DEFAULT_CURVE[3]} 1,1)`,
+ spring: "spring(0.42)",
+ wiggle: "wiggle(3,easeInOut,0.12)",
+} satisfies Record;
+
+export const EasePresetGrid = function EasePresetGrid({
+ kind,
+ currentEase,
+ onSelect,
+}: {
+ kind: EaseMode;
+ currentEase: string;
+ onSelect: (ease: string) => void;
+}) {
+ return (
+