# Paths, trips and labels

> Build smooth paths through points, move objects and multi-car trains along them, run timetabled trips that brake into stops and wait, show a live status line, and pin React Native views to points in the scene.

Three pieces cover routes, transit and map-style apps:

| Piece | Does |
| --- | --- |
| `usePath(points)` | A smooth curve through points, measured in units (metres) |
| `useTrip({ path, stops })` | A timetable: speeds up, cruises, brakes exactly onto each stop, waits, goes on |
| `<FollowPath>` | Puts its children on a path, facing along it |
| `useTripStatus(trip)` | The trip's state for UI, once a second |
| `<Label>` | A React Native view pinned to a point in the scene |

## A path

```tsx title="Route.tsx"
import { Box, FollowPath, Scene, usePath, type Vec3 } from "@apricot/react";
import { useDerivedValue, useSharedValue, withRepeat, withTiming } from "react-native-reanimated";
import { useEffect } from "react";

const POINTS: Vec3[] = [
  [-6, 0, 2],
  [-2, 0, -2],
  [2, 0, 2],
  [6, 0, -2],
];

export function Route() {
  const path = usePath(POINTS);
  const progress = useSharedValue(0);
  useEffect(() => {
    progress.value = withRepeat(withTiming(1, { duration: 6000 }), -1, true);
  }, [progress]);
  const distance = useDerivedValue(() => progress.value * path.length);

  return (
    <Scene style={{ flex: 1 }} camera={{ position: [0, 9, 9] }}>
      <FollowPath path={path} distance={distance}>
        <Box size={[0.6, 0.4, 1]} color="#ff9f59" />
      </FollowPath>
    </Scene>
  );
}
```

- `usePath(points, { closed, tension })` returns a `Path`. `closed: true` joins the end back to the start (a loop line, a race track). `tension` (0–1, default 0.5) sets how tightly it hugs the points: 0.5 is smooth with no overshoot. The path is rebuilt only when the points' **values** change, so inline arrays are fine.
- `<FollowPath distance>` takes a plain number or a shared value, in units along the path. Its children face along the path: their **+z** points forward and +y up. `orient={false}` keeps their own rotation.

### Path methods

| Method | Returns |
| --- | --- |
| `length` | Total length |
| `pointAt(distance, target?)` | The point at that distance (`Vector3`) |
| `tangentAt(distance, target?)` | The direction of travel there (unit `Vector3`) |
| `place(object, distance, direction?)` | Moves and turns a three.js object onto the path. `direction` −1 faces backwards |
| `every(spacing, start?)` | Distances every `spacing` units, for posts, pillars and lamps |
| `distanceTo(point)` | The distance along the path closest to a point, to put stops at given positions |
| `clamp(distance)` | Wraps the distance around a closed path, or clamps it to the ends |

Pillars along the route, as instances:

```tsx title="Pillars.tsx"
import { Cylinder, Instances, type Path } from "@apricot/react";

export function Pillars({ path }: { path: Path }) {
  const items = path.every(10, 5).map((d) => {
    const p = path.pointAt(d);
    return { position: [p.x, 1, p.z] as const };
  });
  return (
    <Instances items={items} material="matte">
      <Cylinder radius={0.3} height={2} />
    </Instances>
  );
}
```

## A trip: stops and a timetable

`useTrip` runs a vehicle along stops. It is **time-based**: exact at any frame rate, and it keeps its place while the app sleeps. It also works outside `<Scene>`, next to the UI that shows its status.

```ts
const trip = useTrip({ path: line, stops: [40, 900, 2100], speed: 22, dwell: 12 });
```

| Option | Default | Meaning |
| --- | --- | --- |
| `stops` | required | Distances of the stops along the path, in order |
| `path` | | The path (gives `"loop"` trips their length) |
| `speed` | `20` | Top speed, units per second |
| `acceleration` | `1.2` | Speeding up, units per second² |
| `deceleration` | the acceleration | Braking |
| `dwell` | `8` | Seconds at each stop |
| `pattern` | `"shuttle"` | After the last stop: `"shuttle"` runs back (a metro line), `"loop"` goes on to the first stop (a closed path), `"once"` stays |
| `start` | `0` | Seconds into the trip at mount, for example from a timetable. 0 is waiting at the first stop |
| `clock` | device clock | A function returning seconds now. Pass a server clock to sync devices |
| `onArrive(stop)`, `onDepart(stop)` | | Called with the stop's index, on timers (no frames needed) |

It returns a `TripHandle`: pass it to `<FollowPath trip>` and `useTripStatus`. `handle.state()` and `handle.elapsed()` read it now.

`TripState`:

| Field | Meaning |
| --- | --- |
| `distance` | Distance along the path |
| `speed` | Units per second (0 at a stop) |
| `direction` | 1 forward along the path, −1 back |
| `stopped` | True while waiting at a stop |
| `stop` | The stop it is at (stopped) or last left (moving), as an index into `stops` |
| `nextStop` | The stop it is heading for |
| `eta` | Seconds until the next stop (moving) or until it leaves (stopped) |

With a trip, `<FollowPath>` renders while the vehicle moves and sleeps while it waits at a stop.

### A train of several cars

Each car is a `<FollowPath>` with its own `offset` (units ahead or behind), so the train bends round curves. Centre the offsets (`[-21, 0, 21]`) so the stops measure the train's middle and nothing jumps when it turns back.

## Status for the UI: `useTripStatus`

`useTripStatus(trip, every = 1)` returns the `TripState` plus `seconds` (the eta rounded up), refreshed every `every` seconds by a timer, so it counts down while the scene sleeps.

> [!IMPORTANT]
> Call `useTripStatus` in a **small component of its own** (the status line), not in the component that renders the scene: it re-renders every second.

## Labels: views pinned to the scene

`<Label>` places any React Native views at a point in the scene. It moves on the UI thread (no React render per frame), hides when the point is behind the camera, and stays hidden until the scene has drawn. Put it inside `<Scene>`.

| Prop | Default | Meaning |
| --- | --- | --- |
| `attach` | | A ref or object to follow (a train car, a model) |
| `position` | `[0, 0, 0]` | Where it is pinned, in its parent's space. With `attach`, an offset from the object along world axes, so a label stays above a turning car |
| `anchor` | `"bottom"` | Which part of the label sits on the point: `"bottom"` (floats above it, like a map pin), `"center"`, `"top"` |
| `style` | | Style for the label's view |

## All together: a metro line

```tsx title="MetroLine.tsx"
import {
  Box,
  CameraRig,
  FollowPath,
  Label,
  Plane,
  Scene,
  type SceneObject,
  type TripHandle,
  usePath,
  useTrip,
  useTripStatus,
  type Vec3,
} from "@apricot/react";
import { useMemo, useRef } from "react";
import { StyleSheet, Text, View } from "react-native";

const LINE: Vec3[] = [
  [-520, 0, 260],
  [-300, 0, 160],
  [-120, 0, 180],
  [60, 0, 60],
  [140, 0, -140],
  [320, 0, -260],
  [540, 0, -220],
];
const STATIONS = ["Harbour", "Market", "Museum", "Park"];
const CAR_LENGTH = 20;
const CARS = [-1, 0, 1].map((i) => i * (CAR_LENGTH + 1));
const timeOf = (s: number) => `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;

function TrainPill({ trip }: { trip: TripHandle }) {
  const status = useTripStatus(trip);
  return (
    <View style={styles.pill}>
      <Text style={styles.pillText}>Your train · {status.stopped ? "boarding" : timeOf(status.seconds)}</Text>
    </View>
  );
}

function StatusLine({ trip }: { trip: TripHandle }) {
  const status = useTripStatus(trip);
  return (
    <Text style={styles.status}>
      {status.stopped
        ? `At ${STATIONS[status.stop]} · leaves in ${timeOf(status.seconds)}`
        : `Next: ${STATIONS[status.nextStop]} · ${timeOf(status.seconds)}`}
    </Text>
  );
}

export function MetroLine() {
  const line = usePath(LINE);
  const stops = useMemo(() => STATIONS.map((_, i) => 40 + (i / (STATIONS.length - 1)) * (line.length - 80)), [line]);
  const trip = useTrip({ path: line, stops, speed: 24, acceleration: 1.1, dwell: 10 });
  const lead = useRef<SceneObject>(null);

  return (
    <View style={{ flex: 1 }}>
      <Scene style={StyleSheet.absoluteFill} look="soft" fog={{ near: 600, far: 2400 }} camera={{ fov: 40, far: 3000 }}>
        <Plane size={2400} rotation={[-Math.PI / 2, 0, 0]} color="#dfe6d3" material="matte" />
        {stops.map((d, i) => {
          const at = line.pointAt(d);
          return (
            <Label key={STATIONS[i]} position={[at.x, 12, at.z]}>
              <Text style={styles.station}>{STATIONS[i]}</Text>
            </Label>
          );
        })}
        {CARS.map((offset) => (
          <FollowPath key={offset} path={line} trip={trip} offset={offset} ref={offset === 0 ? lead : undefined}>
            <Box size={[3, 3.4, CAR_LENGTH]} radius={0.5} position={[0, 2, 0]} material={{ preset: "paint", color: "#f3f5f7" }} />
          </FollowPath>
        ))}
        <Label attach={lead} position={[0, 7, 0]}>
          <TrainPill trip={trip} />
        </Label>
        {/* After the cars, so it follows this frame's position. */}
        <CameraRig
          follow={lead}
          view={{ azimuth: 55, elevation: 26, distance: 210 }}
          drift={2}
          orbit={{ elevation: [8, 89], zoom: [0.4, 2], returnAfter: 5 }}
          inset={{ bottom: 120 }}
        />
      </Scene>
      <View style={styles.card}>
        <StatusLine trip={trip} />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  station: { fontWeight: "700", fontSize: 12, backgroundColor: "rgba(255,255,255,0.85)", paddingHorizontal: 8, paddingVertical: 3, borderRadius: 8, overflow: "hidden" },
  pill: { backgroundColor: "#1f9d55", paddingHorizontal: 10, paddingVertical: 5, borderRadius: 12 },
  pillText: { color: "#ffffff", fontWeight: "700", fontSize: 13 },
  card: { position: "absolute", left: 12, right: 12, bottom: 24, padding: 16, borderRadius: 20, backgroundColor: "rgba(255,255,255,0.94)" },
  status: { fontWeight: "600", fontSize: 17 },
});
```

For big scenes like this one, give the fog a matching range. `<CameraRig>` widens the camera's `far` plane for distant views by itself (to five times the view's distance); with a fixed camera, raise `far` (default 200) yourself.

## Related

- [Motion reference](https://matthiasbrandolin.dev/apricot/reference/motion#paths).
- [Labels and blur reference](https://matthiasbrandolin.dev/apricot/reference/labels-and-blur).
- [Camera](https://matthiasbrandolin.dev/apricot/guides/camera#following-and-drifting).
