# Timeline

> Organism — orn-ui

> ⚠️ Requires <UIProvider>: This component (like every orn-ui component) must render inside a <UIProvider> ancestor, or it throws at runtime. See https://orn-ui-docs.vercel.app/getting-started.md

> Runs on Expo SDK 54, 55, 56 and 57, and on bare React Native >=0.81 with react >=19.1. No native modules — works in Expo Go, no prebuild.

## Installation

Install just this component (copies the source into your project, no npm dependency)
```bash
npx orn-ui add timeline
```

Or install the whole package and import it
```tsx
pnpm add orn-ui
import { Timeline } from 'orn-ui/timeline';
```

Depends on: core (theme + icons), pressable-scale, transition

## Variants

### default — the line bows, the pills alternate sides

```tsx
<Timeline items={ROADMAP} />
```

### curve={0} — a straight spine

```tsx
<Timeline items={PIPELINE} curve={0} />
```

### startSide="left" and a wider curve

```tsx
<Timeline items={PIPELINE} startSide="left" curve={44} />
```

### glow={false} — just the pills

```tsx
<Timeline items={ROADMAP} glow={false} />
```

### spacing={60} — tighter, inside a Card

```tsx
<Card>
  <Timeline items={PIPELINE} spacing={60} curve={20} />
</Card>
```

### a single item — no line to draw

```tsx
<View>
  <Timeline items={[{ label: 'Atomic design', emoji: '⚛️' }]} />
</View>
```

## Full demo source

```tsx
function upTo(items: TimelineItem[], reached: number): TimelineItem[] {
  return items.map((item, index) => ({ ...item, status: index <= reached ? undefined : 'pending' }));
}

/**
 * Tocar un hito lo alcanza, y con él todo lo anterior: la línea viaja hasta
 * ahí. Tocar el que ya está alcanzado da marcha atrás, para poder mirar la
 * animación en los dos sentidos sin recargar.
 */
function TappableTimeline() {
  const [reached, setReached] = useState(1);

  return (
    <View style={{ gap: 8 }}>
      <Timeline
        items={upTo(ROADMAP, reached)}
        onItemPress={(index) => setReached(index === reached ? index - 1 : index)}
      />
      {/* El eco existe para el flow de Maestro: el nodo que se llena y la
          línea que viaja son píxeles, y un assert no los ve. El texto sí. */}
      <Body>Reached: {ROADMAP[reached]?.label ?? 'nothing yet'}</Body>
    </View>
  );
}

/**
 * Avanza solo y vuelve a empezar: el demo existe para mirar el movimiento.
 *
 * Sin botón de pausa a propósito. Iba uno, y caía justo donde el pager de
 * VariantList arranca su swipe (80% de la pantalla): el botón se quedaba con
 * el gesto y no se podía pasar de variante.
 */
function AutoTimeline() {
  const [reached, setReached] = useState(-1);

  useEffect(() => {
    const id = setInterval(() => {
      setReached((previous) => (previous >= ROADMAP.length - 1 ? -1 : previous + 1));
    }, 1100);
    return () => clearInterval(id);
  }, []);

  return (
    <View style={{ gap: 8 }}>
      <Timeline items={upTo(ROADMAP, reached)} duration={700} />
      <Body>Now at: {ROADMAP[reached]?.label ?? 'the start'}</Body>
    </View>
  );
}

/**
 * Avance estricto: sólo responde el hito que sigue, y lo alcanzado no se
 * devuelve. Es el onboarding que no deja saltear pasos.
 */
function SequentialTimeline() {
  const [reached, setReached] = useState(0);

  return (
    <View style={{ gap: 8 }}>
      <Timeline
        items={upTo(ROADMAP, reached)}
        advance="sequential"
        onItemPress={setReached}
      />
      <Body>
        {reached >= ROADMAP.length - 1
          ? 'All done — nothing left to tap'
          : `Only "${ROADMAP[reached + 1]?.label}" answers`}
      </Body>
    </View>
  );
}

/**
 * Progreso guardado, contenido revisitable: tocar un hito anterior cambia lo
 * que estoy mirando, no lo que llevo recorrido. La línea se queda donde
 * llegó y el borde marca dónde estoy parado.
 */
function RevisitTimeline() {
  const [reached, setReached] = useState(2);
  const [looking, setLooking] = useState(2);

  return (
    <View style={{ gap: 8 }}>
      <Timeline
        items={upTo(ROADMAP, reached)}
        advance="revisit"
        selectedIndex={looking}
        onItemPress={(index) => {
          setLooking(index);
          // Sólo el hito que sigue empuja el recorrido; los de atrás son una
          // visita.
          if (index > reached) setReached(index);
        }}
      />
      <Body>Looking at: {ROADMAP[looking]?.label}</Body>
      <Body>Progress stays at: {ROADMAP[reached]?.label}</Body>
    </View>
  );
}

export function TimelineDemo() {
  const variants: VariantDef[] = [
    {
      label: 'default — the line bows, the pills alternate sides',
      content: <Timeline items={ROADMAP} />,
    },
    {
      label: 'onItemPress — tap a milestone and the line travels there',
      content: <TappableTimeline />,
    },
    {
      label: 'advance="sequential" — only the next milestone answers, no going back',
      content: <SequentialTimeline />,
    },
    {
      label: 'advance="revisit" — revisit the past, keep the progress',
      content: <RevisitTimeline />,
    },
    {
      label: 'advancing on its own — the line draws itself, gap by gap',
      content: <AutoTimeline />,
    },
    {
      label: 'curve={0} — a straight spine',
      content: <Timeline items={PIPELINE} curve={0} />,
    },
    {
      label: 'startSide="left" and a wider curve',
      content: <Timeline items={PIPELINE} startSide="left" curve={44} />,
    },
    {
      label: 'glow={false} — just the pills',
      content: <Timeline items={ROADMAP} glow={false} />,
    },
    {
      label: 'spacing={60} — tighter, inside a Card',
      content: (
        <Card>
          <Timeline items={PIPELINE} spacing={60} curve={20} />
        </Card>
      ),
    },
    {
      label: 'a single item — no line to draw',
      content: (
        <View>
          <Timeline items={[{ label: 'Atomic design', emoji: '⚛️' }]} />
        </View>
      ),
    },
  ];
  return <VariantList variants={variants} />;
```

## Props

| Name | Type | Default | Description |
|---|---|---|---|
| `items` | `TimelineItem[]` | — | The milestones, in order. Each one takes a label, an optional icon or emoji, and its status. |
| `curve?` | `number` | 28 | Pixels the line bows away from the straight path. 0 draws it straight. |
| `spacing?` | `number` | 84 | Vertical distance between one node and the next. |
| `startSide?` | `'left' \| 'right'` | right | Side the first pill goes to; the rest alternate. |
| `glow?` | `boolean` | true | Halo behind each pill. |
| `onItemPress?` | `(index: number) => void` | — | Makes each pill tappable and reports its index. |
| `advance?` | `'free' \| 'sequential' \| 'revisit'` | free | Which taps are allowed, and whether the line may go back. |
| `selectedIndex?` | `number` | — | The milestone being looked at right now, highlighted. It is not the progress: it can sit behind the line without pulling it back. |
| `duration?` | `number` | 420 | Milliseconds the line takes to travel one gap when a milestone is reached. |
| `style?` | `StyleProp<ViewStyle>` | — | Style for the container. |
