# Animation and shared values

> The four ways to animate in Apricot (props, shared values, useFrame and behaviors), which props accept Reanimated shared values, and how the render loop stays asleep when nothing moves.

Apricot's rule is: **React handles structure, never per-frame changes.** Use React state to decide *what* is in the scene. Use shared values, `useFrame` and behaviors to decide *how it moves*. A scene that animates through React re-renders will stutter; one that animates through the paths below runs at the display's refresh rate.

## 1. Plain props

Passing a new plain value is fine for occasional changes: a button press, a selection, a mode switch.

```tsx title="Selected.tsx"
import { Box, Scene } from "@apricot/react";
import { useState } from "react";

export function Selected() {
  const [selected, setSelected] = useState(false);
  return (
    <Scene style={{ flex: 1 }}>
      <Box
        position={[0, selected ? 0.5 : 0, 0]}
        color={selected ? "#22c55e" : "#cfd3d8"}
        onPress={() => setSelected((s) => !s)}
      />
    </Scene>
  );
}
```

A new `position` jumps there at once. Some props **animate** to a new plain value by themselves:

| Prop | Animates over | Default time |
| --- | --- | --- |
| Material `color`, `roughness`, `metalness` | `transition` in the material options | 0.4 s |
| `<Model joints>` values | the joint's `transition` | 0.6 s |
| `<Model animations>` progress (a number) | fixed | 0.6 s |
| `<Model morphs>` weights | fixed | 0.4 s |
| `<CameraRig view>` | `transition` | 0.9 s |
| `<Environment name>` (or sky, boxes) | `transition` | 0.6 s |
| `<Daylight time>` | `transition` | 1.5 s |

## 2. Shared values

Every **animatable** prop accepts a Reanimated shared value (`useSharedValue`, `useDerivedValue`) as well as a plain value. When a shared value changes, Apricot copies it to the render loop without a React render and without blocking the JS thread, then wakes the loop. Gestures, springs, timings, decays and sensors all run on the UI thread and reach the 3D scene with 2–3 ms of latency.

```tsx title="Bounce.tsx"
import { Scene, Sphere } from "@apricot/react";
import { useEffect } from "react";
import { useDerivedValue, useSharedValue, withRepeat, withSpring } from "react-native-reanimated";

export function Bounce() {
  const height = useSharedValue(0);
  useEffect(() => {
    height.value = withRepeat(withSpring(1.2, { damping: 6 }), -1, true);
  }, [height]);
  const position = useDerivedValue<readonly [number, number, number]>(() => [0, height.value, 0]);

  return (
    <Scene style={{ flex: 1 }}>
      <Sphere radius={0.5} position={position} material="paint" color="#e3a21a" />
    </Scene>
  );
}
```

Vectors (`position`, `rotation`, a per-axis `scale`) are tuples, so derive them with `useDerivedValue<readonly [number, number, number]>(…)` from one or more numeric shared values.

### Which props take shared values

| Component | Animatable props |
| --- | --- |
| Every object (`Group`, `Mesh`, shapes, `Model`, `Primitive`, `Instances`, assets) | `position`, `rotation`, `scale` |
| `<Spin>` and any `defineBehavior` component | every prop in its `defaults` (`speed`, `axis`) |
| `<AmbientLight>`, `<HemisphereLight>` | `intensity` |
| `<DirectionalLight>`, `<SpotLight>` | `intensity`, `position`, `target` |
| `<PointLight>` | `intensity`, `position` |
| `<Environment>` | `intensity`, `rotation` |
| `<Daylight>` | `time` |
| `<Vehicle>` | `throttle`, `steer` |
| `<FollowPath>` | `distance` |
| `<Model>` | each value in `joints`, `animations` (progress) and `morphs` |

Props not in this table (colours, sizes, materials, names, callbacks) are plain values: change them with React state.

> [!NOTE]
> A shared value is followed **exactly**: it is not smoothed again. Animate it with `withSpring` or `withTiming` if you want easing. A plain value on a joint or material eases by itself.

## 3. `useFrame`

`useFrame` runs a function before every rendered frame. Use it for logic that is not a single prop: physics of your own, many objects, procedural motion.

```tsx title="Wobble.tsx"
import { Box, Scene, type SceneObject, useFrame } from "@apricot/react";
import { useRef } from "react";

function Wobbler() {
  const box = useRef<SceneObject>(null);
  useFrame(({ time }) => {
    if (!box.current) return true;
    box.current.rotation.z = Math.sin(time * 3) * 0.3;
    return true; // still animating: keep rendering
  });
  return <Box ref={box} size={1.2} />;
}

export function Wobble() {
  return (
    <Scene style={{ flex: 1 }}>
      <Wobbler />
    </Scene>
  );
}
```

The callback receives `{ time, delta, frame }`:

| Field | Meaning |
| --- | --- |
| `time` | Seconds since the loop started. |
| `delta` | Seconds since the previous rendered frame. `0` on the first frame, capped at `0.1`. Multiply speeds by it. |
| `frame` | The number of frames rendered so far. |

**Return `true` while animating** and `false` (or nothing) when finished. In `"demand"` mode the loop sleeps once no callback returns `true` and nothing else changed. A motion that ends by itself should return `false` when it ends:

```tsx title="DecayingSpin.tsx"
import { Box, type SceneObject, useFrame, useScene } from "@apricot/react";
import { useEffect, useRef } from "react";

export function DecayingSpin({ kicks }: { kicks: number }) {
  const scene = useScene();
  const cube = useRef<SceneObject>(null);
  const speed = useRef(0);

  useEffect(() => {
    if (kicks === 0) return;
    speed.current += 6;
    scene.invalidate(); // wake the loop: this change happened outside useFrame
  }, [kicks, scene]);

  useFrame(({ delta }) => {
    if (!cube.current || speed.current === 0) return false;
    cube.current.rotation.y += speed.current * delta;
    speed.current *= 0.04 ** delta; // loses about 96% of its speed per second
    if (speed.current < 0.01) speed.current = 0;
    return speed.current > 0;
  });

  return <Box ref={cube} size={1.6} />;
}
```

> [!IMPORTANT]
> `useFrame` and `useScene` only work in components rendered **inside** `<Scene>`. Put the logic in a child component, as `Wobbler` and `DecayingSpin` do above, not in the component that renders the `<Scene>`.

`useFrame` runs on the JS thread. To change a single prop from a gesture, a shared value is simpler and has lower latency.

## 4. Behaviors

A behavior is a component that moves its children every frame. `<Spin>` is one. Nest behaviors and groups to combine motions:

```tsx title="Combined.tsx"
import { MoneyStack } from "@apricot/assets";
import { Group, Scene, Spin } from "@apricot/react";

export function Combined({ rotation }: { rotation: readonly [number, number, number] }) {
  return (
    <Scene style={{ flex: 1 }}>
      <Spin speed={0.6}>
        {/* the steady spin and the user's swipe add up */}
        <Group rotation={rotation}>
          <MoneyStack bills={40} />
        </Group>
      </Spin>
    </Scene>
  );
}
```

Write your own with `defineBehavior`: see [Custom geometry and behaviors](https://matthiasbrandolin.dev/apricot/guides/custom).

## Choosing

| Situation | Use |
| --- | --- |
| A tap toggles something | React state and plain props |
| A gesture moves, turns or opens something | A shared value written by the gesture |
| A spring or timing animation | `withSpring` / `withTiming` on a shared value |
| Constant motion (spinning, floating) | `<Spin>` or a behavior |
| Motion that depends on time or on many objects | `useFrame` with refs |
| Hundreds of moving copies | `useFrame` with `<Instances ref>` and `ref.set(index, item)` |

## Related

- [Scenes](https://matthiasbrandolin.dev/apricot/concepts/scenes#when-the-scene-renders): what wakes the loop.
- [Gestures and interaction](https://matthiasbrandolin.dev/apricot/guides/interaction).
- [Hooks reference](https://matthiasbrandolin.dev/apricot/reference/hooks): `useFrame`, `useAnimatable`, `useScene`.
