diff --git a/docs/INTERACTIVITY.md b/docs/INTERACTIVITY.md
index 05260c9..393497e 100644
--- a/docs/INTERACTIVITY.md
+++ b/docs/INTERACTIVITY.md
@@ -123,6 +123,26 @@ const html = interactiveEmbed(svgString, { title: 'Sales' });
The MCP server exposes the same via the `export_interactive_html` tool, so an
agent can emit an interactive chart a reader can hover — not just a static image.
+## Data-change transitions
+
+Pass `transitions={{ enabled: true }}` to `BarChart`, `LineChart`, `AreaChart`,
+or `PieChart` and the chart will tween between data snapshots (400 ms by
+default; override with `durationMs`). Off by default — existing renders are
+byte-identical. Honors `prefers-reduced-motion` (snaps instead of tweening).
+
+```tsx
+
+```
+
+Under the hood the chart wraps its `data` prop in `useDataTransition`
+(exported from `goldenchart/interactive` for direct use in custom
+compositions). Rough.js seeds are index-based and don't shimmer across frames.
+
## Accessibility
Marks are keyboard-focusable with `aria-label`s; the tooltip path is reachable
diff --git a/src/components/AreaChart.tsx b/src/components/AreaChart.tsx
index 3f48f62..abdae96 100644
--- a/src/components/AreaChart.tsx
+++ b/src/components/AreaChart.tsx
@@ -9,6 +9,7 @@ import { colorAt } from '../core/palette';
import { resolveBrand } from '../brand/resolveBrand';
import { seriesTable } from '../core/dataTable';
import { describeSeries } from '../core/a11yDescribe';
+import { useDataTransition } from '../interactive/useDataTransition';
import { layoutLegend } from '../core/legend';
import type { LegendItem } from '../core/legend';
import { resolveVibe } from '../vibe/resolveVibe';
@@ -44,7 +45,7 @@ export interface AreaChartProps extends BaseChartProps {
* d3-shape's `area` builds the fill path; the vibe's `fillStyle` textures it.
*/
export function AreaChart({
- series,
+ series: rawSeries,
width,
height,
margin,
@@ -67,7 +68,13 @@ export function AreaChart({
annotations,
xAxis,
yAxis,
+ transitions,
}: AreaChartProps) {
+ const series = useDataTransition(
+ rawSeries,
+ transitions?.durationMs ?? 400,
+ transitions?.enabled ?? false,
+ );
const fullPlot = getPlotArea(width, height, margin);
const rv = resolveVibe(vibe);
const palette = resolveBrand(brand).palette;
diff --git a/src/components/BarChart.tsx b/src/components/BarChart.tsx
index 4f99571..791d5a9 100644
--- a/src/components/BarChart.tsx
+++ b/src/components/BarChart.tsx
@@ -13,6 +13,7 @@ import { colorAt } from '../core/palette';
import { resolveBrand } from '../brand/resolveBrand';
import { datumTable } from '../core/dataTable';
import { describeBars } from '../core/a11yDescribe';
+import { useDataTransition } from '../interactive/useDataTransition';
import { groupMax, seriesKeysOf, stackLayout, stackMax } from '../core/stack';
import { Surface } from './Surface';
import { Axis } from './Axis';
@@ -63,7 +64,7 @@ interface LaidBar {
* multi-series modes.
*/
export function BarChart({
- data,
+ data: rawData,
width,
height,
margin,
@@ -84,7 +85,13 @@ export function BarChart({
annotations,
xAxis,
yAxis,
+ transitions,
}: BarChartProps) {
+ const data = useDataTransition(
+ rawData,
+ transitions?.durationMs ?? 400,
+ transitions?.enabled ?? false,
+ );
const fullPlot = getPlotArea(width, height, margin);
const resolved = resolveVibe(vibe);
const palette = resolveBrand(brand).palette;
diff --git a/src/components/LineChart.tsx b/src/components/LineChart.tsx
index 1425ca5..2aa2716 100644
--- a/src/components/LineChart.tsx
+++ b/src/components/LineChart.tsx
@@ -9,6 +9,7 @@ import { colorAt } from '../core/palette';
import { resolveBrand } from '../brand/resolveBrand';
import { seriesTable } from '../core/dataTable';
import { describeSeries } from '../core/a11yDescribe';
+import { useDataTransition } from '../interactive/useDataTransition';
import { layoutLegend } from '../core/legend';
import type { LegendItem } from '../core/legend';
import { resolveVibe } from '../vibe/resolveVibe';
@@ -42,7 +43,7 @@ export interface LineChartProps extends BaseChartProps {
/** Multi-series line chart: d3-shape builds each path, `` sketches it. */
export function LineChart({
- series,
+ series: rawSeries,
width,
height,
margin,
@@ -64,7 +65,13 @@ export function LineChart({
emphasis,
xAxis,
yAxis,
+ transitions,
}: LineChartProps) {
+ const series = useDataTransition(
+ rawSeries,
+ transitions?.durationMs ?? 400,
+ transitions?.enabled ?? false,
+ );
const fullPlot = getPlotArea(width, height, margin);
const rv = resolveVibe(vibe);
const palette = resolveBrand(brand).palette;
diff --git a/src/components/PieChart.tsx b/src/components/PieChart.tsx
index 8c7fd9f..824393e 100644
--- a/src/components/PieChart.tsx
+++ b/src/components/PieChart.tsx
@@ -6,6 +6,7 @@ import { colorAt } from '../core/palette';
import { resolveBrand } from '../brand/resolveBrand';
import { datumTable } from '../core/dataTable';
import { describePie } from '../core/a11yDescribe';
+import { useDataTransition } from '../interactive/useDataTransition';
import { resolveVibe } from '../vibe/resolveVibe';
import { Surface } from './Surface';
import { RoughPath } from '../primitives/RoughPath';
@@ -25,7 +26,7 @@ export interface PieChartProps extends BaseChartProps {
* origin; we translate to the plot center and let `` sketch them.
*/
export function PieChart({
- data,
+ data: rawData,
width,
height,
margin,
@@ -41,7 +42,13 @@ export function PieChart({
innerRadius = 0,
padAngle = 0.02,
showLabels = true,
+ transitions,
}: PieChartProps) {
+ const data = useDataTransition(
+ rawData,
+ transitions?.durationMs ?? 400,
+ transitions?.enabled ?? false,
+ );
const plot = getPlotArea(width, height, margin);
const cx = plot.x + plot.width / 2;
const cy = plot.y + plot.height / 2;
diff --git a/src/types/charts.ts b/src/types/charts.ts
index 8e84a79..891a2d6 100644
--- a/src/types/charts.ts
+++ b/src/types/charts.ts
@@ -60,6 +60,16 @@ export interface BaseChartProps {
children?: ReactNode;
/** Render only the bare `