# Lifeline ships seven registry items and no releases: what a shadcn timeline actually installs

> evilrabbit/lifeline is a TypeScript timeline that lays milestones on one rail, switching from a scroll-scrubbed horizontal layout to a vertical one at the md breakpoint. It installs as source through a shadcn registry, carries no GitHub releases and sits at version 0.1.0, so the details worth reading before you add an item are the prop contracts, the shared width constant, and the wheel ownership.

**evilrabbit/lifeline** — A timeline component for the stories that unfold over time. Ships as a shadcn registry.

- Repository: https://github.com/evilrabbit/lifeline
- Website: https://lifeline-evil-rabbit.vercel.app
- Stars: 535 · Forks: 26
- Language: TypeScript
- License: MIT
- Published: 2026-09-17 · Updated: 2026-09-17 · Language: en
- Canonical page: https://hysenlabs.com/projects/evilrabbit-lifeline

## The package is 0.1.0, marked private, with no GitHub releases

There is no release history to read. The repository has no GitHub releases at all, and package.json carries version 0.1.0 alongside private set to true, so the demo app is never published and there is no tag to compare against. What is versioned in practice is the registry, not the repository: `registry.json` sits at the root and holds the item index that `npx shadcn@latest add evilrabbit/lifeline/{name}` resolves against, with the same JSON reachable at `https://evilrabbit.com/r/{name}.json` for anyone who prefers URLs. Last push on main was 2026-07-31, so treat the registry index as the only statement of what currently ships. A missing tag list is not a problem for a copy-in component library, but it does mean you cannot tell from the outside whether an item changed shape between two installs.

## The demo and the registry you install from are on different hosts

Two origins carry this project. The homepage is a Vercel deployment at lifeline-evil-rabbit.vercel.app, which is the page with the sun and moon toggle, while the JSON your CLI fetches is served from evilrabbit.com. Comparing the live demo against your installed result therefore means comparing two deployments that are versioned separately, and the demo is the one that carries a visible toggle. That toggle is also its own registry item, `evilrabbit/lifeline/theme-switcher`, so the sun and moon in the demo is a separate `npx` call rather than a prop. The seven item names split by how much they take with them: `page` bundles components, starter data, the shell and a route at `app/lifeline/page.tsx`; `shell` is the shell without the route; `personal`, `company` and `journey` each bring the component system plus a commented template file and no framing; `lifeline` is the component system alone.

## The repository is a pnpm workspace, but every install line says npx

The tree ships `pnpm-lock.yaml` and `pnpm-workspace.yaml`, and package.json declares no packageManager field to say which pnpm version the lockfile belongs to. Every documented install path runs the other package manager:

```bash
npx shadcn@latest add evilrabbit/lifeline/page
```

That is harmless in isolation, since `npx` only downloads the shadcn CLI, but it means the documented workflow never touches the workspace lockfile. Two other details in the same file shape what lands in your project. `shadcn` itself sits in dependencies at `^4.13.1` rather than in devDependencies, so the registry tool is a runtime dependency of the demo app. And `tailwindcss` at `^4` sits in devDependencies while `tw-animate-css` and `tw-animate-css` utilities ship as runtime dependencies, matching the registry items adding intro keyframes into your CSS. Any item you install also brings `lucide-react` and `next-themes`.

## The width cap is one constant shared by the nav and the footer

LifelineShell, LifelineNav, LifelineStage and LifelineFooter all take `className` and pass it through, which reads like a uniform surface and is not quite one. Two of them take a second prop, `containerClassName`, and that prop overrides the width cap on the nav and on the footer. The cap is a single constant shared between the two, so changing it on only one of them desynchronizes the frame. The visible symptom is stated plainly: the rail's end follows the nav, so a mismatch shows up as a rail that stops short of the footer's edge. This is the most likely first surprise for anyone assembling the shell by hand rather than installing `page`, because a rail that stops early looks like a layout bug in the component rather than a missing edit in the call site.

## A height set on className is desktop only unless mode is embed

The `className` on the timeline itself is merged onto the horizontal timeline's root after its own `pt-5`, so a tailwind utility such as `h-full` wins over the built-in padding rather than being merged around it. The trap is scope. That merge is desktop only, with one exception: under `mode="embed"` the same className also lands on the vertical layout's scroll box, so a height set there applies below `md` as well. On a full-page timeline, a height you set has no effect on the mobile column at all. The breakpoint itself is fixed at `md` and not configurable, which is what produces the automatic switch: a horizontal scroll-scrubbed timeline above it, a vertical scrolling timeline below. The mobile side is also where the non-timeline data surfaces, since `photos` cards, `badges` and the `companies` marks are laid out along the rail rather than in a separate mobile tree.

## A full-page rail owns the wheel, and mode auto measures to decide

`mode` takes `auto`, `page` or `embed` and decides whether the timeline is the page or a module inside one. The default is `auto`, which measures rather than assuming. A full-page timeline owns the wheel on purpose: `LifelineStage` clears the fixed nav and hands scrolling to the horizontal scrub above `md`. A timeline sitting in the middle of a page that has its own content must not, so it gets `mode="embed"` plus an explicit height on a wrapper:

```tsx
<div className="h-[600px]">
  <Lifeline mode="embed" className="h-full" markers={life.markers} birthYear={life.birthYear} />
</div>
```

The only difference at the ends is what happens when the rail runs out: the wheel returns to the page and carries on scrolling. Nothing is pinned and there is no tall spacer, so the module stays where your layout put it and the wheel is borrowed only while rail remains. Getting this wrong is expensive in one direction and cheap in the other: a full-page rail left inside a scrolling article steals the gesture from content you did not mean to move.

## A gesture already in flight is never captured by the rail

The scrub has a deliberate rule about timing. A gesture already in flight is never captured: flick the page and the timeline lets it pass, and the next deliberate scroll is the one that scrubs. The symmetric case is the more interesting one, because a flick that eats the last of the rail stops there instead of spilling into the page. The release waits for the wheel to go quiet, so one gesture cannot blow through the end of the timeline and dump the reader far down the document. The same design shows up in the media layer: `events` carry strings or `text` with an optional `image`, and a `video` on the image turns it into a looping clip, while an `effect: "fireworks"` hides a WebGL easter egg behind a click. Everything floating is opt-in per milestone, which is what lets the same component serve a life, a company and a bounded run.

## Conclusion

Add lifeline when your story is genuinely sequential and you want the rail to own the scroll, and pick the item that matches your subject rather than the page bundle. Before you run it, check three things: whether the wheel belongs to your page, in which case embed mode plus a height is mandatory rather than cosmetic; whether you will change the width cap, which has to be edited on both the nav and the footer or the rail visibly stops short; and whether a height you set will apply on mobile, which it will not unless embed mode is on. Do not expect version numbers to tell you anything, because there are no releases and the package is marked private at 0.1.0.

## FAQ

### What does adding a lifeline registry item put into my project?

Any item installs the components into `components/lifeline/`, the data helper into `lib/lifeline-data.ts`, the intro keyframes into your CSS, and it installs `lucide-react` and `next-themes`. If `app/lifeline/page.tsx` already exists, shadcn asks before touching it, and answering no loses nothing of yours.

### Why does my lifeline timeline scroll the page instead of sideways inside an article?

A full-page timeline owns the wheel by design. Give it `mode="embed"` and a height on the wrapper, and the rail scrubs sideways until it runs out, at which point the wheel returns to the page and scrolling continues.

### Why does the lifeline rail stop short of my footer?

`containerClassName` overrides the width cap on both the nav and the footer, and the cap is one constant shared between them. The rail's end follows the nav, so changing it on only one of the two leaves a rail that stops short of the footer's edge.

### Does the lifeline timeline change layout on phones?

The switch happens automatically at the `md` breakpoint: a horizontal scroll-scrubbed timeline above it and a vertical scrolling timeline below. A height set through `className` applies on the vertical layout only under `mode="embed"`.

## Sources

- [evilrabbit/lifeline on GitHub](https://github.com/evilrabbit/lifeline)
- [Issues](https://github.com/evilrabbit/lifeline/issues)
- [License: MIT](https://github.com/evilrabbit/lifeline/blob/main/LICENSE)
- [Project website](https://lifeline-evil-rabbit.vercel.app)
- [README](https://github.com/evilrabbit/lifeline/blob/main/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/evilrabbit-lifeline
