diff --git a/packages/studio/src/components/StudioRightPanel.tsx b/packages/studio/src/components/StudioRightPanel.tsx
index b55aac305..68088311f 100644
--- a/packages/studio/src/components/StudioRightPanel.tsx
+++ b/packages/studio/src/components/StudioRightPanel.tsx
@@ -401,8 +401,8 @@ export function StudioRightPanel({
onUpdateArcSegment={handleUpdateArcSegment}
onUnroll={handleUnroll}
onUpdateKeyframeEase={handleUpdateKeyframeEase}
- onSetAllKeyframeEases={handleSetAllKeyframeEases}
onUpdateSegmentEase={handleUpdateSegmentEase}
+ onSetAllKeyframeEases={handleSetAllKeyframeEases}
recordingState={recordingState}
recordingDuration={recordingDuration}
onToggleRecording={onToggleRecording}
diff --git a/packages/studio/src/components/editor/GsapAnimationList.tsx b/packages/studio/src/components/editor/GsapAnimationList.tsx
new file mode 100644
index 000000000..9a88e129f
--- /dev/null
+++ b/packages/studio/src/components/editor/GsapAnimationList.tsx
@@ -0,0 +1,84 @@
+import { useCallback, useState } from "react";
+import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
+import { useShallow } from "zustand/react/shallow";
+import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
+import { usePlayerStore } from "../../player";
+import { isFocusedEaseRequestCurrent } from "../../player/store/keyframeSlice";
+import { AnimationCard } from "./AnimationCard";
+import { GsapAddAnimationControl } from "./GsapAddAnimationControl";
+import {
+ type GsapAnimationEditCallbacks,
+ withTrackedGsapAnimationCallbacks,
+} from "./gsapAnimationCallbacks";
+
+interface GsapAnimationListProps extends GsapAnimationEditCallbacks {
+ elementId: string;
+ animations: GsapAnimation[];
+ onAddAnimation: (method: "to" | "from" | "set" | "fromTo") => void;
+ variant: "classic" | "flat";
+}
+
+/** Shared animation cards, telemetry, and timeline-ease focus ownership for both inspectors. */
+export function GsapAnimationList({
+ elementId,
+ animations,
+ onAddAnimation,
+ variant,
+ ...callbacks
+}: GsapAnimationListProps) {
+ const track = useTrackDesignInput();
+ const [addMenuOpen, setAddMenuOpen] = useState(false);
+ const trackedCallbacks = withTrackedGsapAnimationCallbacks(callbacks, track);
+ const { focusedEaseSegment, timelineProjectId, timelineSessionEpoch, selectedElementId } =
+ usePlayerStore(
+ useShallow((state) => ({
+ focusedEaseSegment: state.focusedEaseSegment,
+ timelineProjectId: state.timelineProjectId,
+ timelineSessionEpoch: state.timelineSessionEpoch,
+ selectedElementId: state.selectedElementId,
+ })),
+ );
+ const focusedHere =
+ focusedEaseSegment &&
+ focusedEaseSegment.elementId === elementId &&
+ isFocusedEaseRequestCurrent(focusedEaseSegment, {
+ timelineProjectId,
+ timelineSessionEpoch,
+ selectedElementId,
+ }) &&
+ animations.some((animation) => animation.id === focusedEaseSegment.animationId)
+ ? focusedEaseSegment
+ : null;
+ // Stable while the request is unchanged: AnimationCard includes this callback
+ // in its focus-effect deps, and a fresh closure would replay that effect on
+ // unrelated inspector renders. Reading the action lazily also avoids a store
+ // subscription for a function whose identity never changes.
+ const consumeFocusedEaseSegment = useCallback(() => {
+ if (focusedHere) {
+ usePlayerStore.getState().clearFocusedEaseSegment(focusedHere.nonce);
+ }
+ }, [focusedHere]);
+
+ return (
+
+ {animations.map((animation, index) => (
+
+ ))}
+
+
+ );
+}
diff --git a/packages/studio/src/components/editor/GsapAnimationSection.test.tsx b/packages/studio/src/components/editor/GsapAnimationSection.test.tsx
index 7d299e158..8aa5c40ea 100644
--- a/packages/studio/src/components/editor/GsapAnimationSection.test.tsx
+++ b/packages/studio/src/components/editor/GsapAnimationSection.test.tsx
@@ -5,11 +5,15 @@ import { createRoot } from "react-dom/client";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
import { DesignPanelInputProvider } from "../../contexts/DesignPanelInputContext";
-import { usePlayerStore } from "../../player";
+import { usePlayerStore } from "../../player/store/playerStore";
import { GsapAnimationSection } from "./GsapAnimationSection";
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
+const animationCardMock = vi.hoisted(() => ({
+ consumers: [] as Array<{ tweenPercentage: number | null; consume: () => void }>,
+}));
+
vi.mock("./AnimationCard", () => ({
AnimationCard: ({
animation,
@@ -19,22 +23,27 @@ vi.mock("./AnimationCard", () => ({
animation: GsapAnimation;
focusedSegment: { tweenPercentage: number } | null;
onFocusSegmentConsumed: () => void;
- }) => (
-