# Primer — Animated App UI Block for React

> Multi-step onboarding wizard in a card: a numbered step rail whose track fills to the current step (completed nodes draw a check, reached steps stay clickable), direction-aware sliding step bodies with a springing card height, and schema-driven step kinds — name + role chips, workspace name with a live typed slug preview and async availability check, email-chip invites with a role, preference toggle cards, and a review summary with Edit jumps. Per-step validation shakes and explains, Enter advances, Back keeps answers, and Finish fires a spark burst and onComplete(values).

- Page: https://crucibleui.com/components/app/primer
- Category: [app](https://crucibleui.com/components/app)
- Tier: pro (one-time purchase, see https://crucibleui.com/pricing)
- Engine: motion
- npm dependencies: motion

## Install

```bash
npx shadcn@latest add @crucible/primer
```

Pro install requires adding the token-bearing `@crucible` registry to components.json (issued at https://crucibleui.com/account after purchase).

Import from `@/components/crucible/primer`.

## Props (`PrimerProps`)

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `steps?` | `PrimerStep[]` | — | Ordered step schema. Omit for Welcome → Workspace → Invite → Preferences → Done. |
| `initialStep?` | `number` | `0` | Step index to open on. Earlier steps count as reachable. |
| `defaultValues?` | `Partial<PrimerValues>` | — | Initial answers (merged over empty defaults). |
| `onComplete?` | `(values: PrimerValues) => void \| Promise<unknown>` | — | Called with every answer when the person presses Finish. Return a Promise to hold the button pending — resolve shows the success state (with a spark burst), reject shows the rejection's message inline. |
| `onStepChange?` | `(index: number, step: PrimerStep) => void` | — | Fires whenever the visible step changes, with its index and schema entry. |
| `onChange?` | `(values: PrimerValues) => void` | — | Fires whenever any answer changes. |
| `validate?` | `PrimerValidate` | `a simulated check that rejects a few reserved words` | Async slug availability check. |
| `domain?` | `string` | `"app.example.com"` | Host shown in the live URL preview. |
| `accentColor?` | `string` | `"#34d399"` | Signature accent for the rail, primary button, focus rings and sparks. |
| `ref?` | `React.Ref<PrimerHandle>` | — | Imperative handle (`goTo(i)`, `reset()`, `replay()`). React 19 ref-as-prop. |
| `className?` | `string` | — | Extra classes for the card root. |

Also accepts: `Omit<React.ComponentPropsWithoutRef<"div">, "onChange" | "children">`.

## Usage example

```tsx
"use client";

import * as React from "react";

import { Primer, type PrimerHandle } from "@/components/crucible/primer";

export default function PrimerDemo() {
  const ref = React.useRef<PrimerHandle>(null);

  return (
    <div className="relative isolate flex h-full min-h-[640px] w-full flex-col items-center justify-center gap-5 overflow-hidden rounded-xl bg-neutral-950 px-4 py-10">
      <div
        aria-hidden
        className="pointer-events-none absolute inset-0 -z-10"
        style={{
          background:
            "radial-gradient(55% 45% at 50% 30%, rgba(52,211,153,0.07), transparent 70%), radial-gradient(40% 40% at 50% 100%, rgba(255,255,255,0.03), transparent 70%)",
        }}
      />
      <div className="relative z-10 flex w-full flex-col items-center gap-5">
        <Primer
          ref={ref}
          onComplete={() => new Promise<void>((resolve) => window.setTimeout(resolve, 800))}
        />
        <div className="flex flex-wrap items-center justify-center gap-x-3 gap-y-1 text-center text-xs text-white/45">
          <span>Work through the steps — Back keeps your answers.</span>
          <button
            type="button"
            onClick={() => ref.current?.replay()}
            className="rounded text-white/40 underline underline-offset-4 transition-colors hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/60"
          >
            Replay
          </button>
        </div>
      </div>
    </div>
  );
}
```

## Accessibility & performance

Honors `prefers-reduced-motion` with a designed static frame or fade (never a blank box) and passes `className` through.
