# Workspace Setup

> An animated workspace setup flow for React and shadcn: name, URL with a live availability check, team and look, beside a preview that becomes the workspace.

A setup flow that shows its result while you make it. The name types into a skeleton of the app, the URL check spins and settles on a check or a free suggestion, the sidebar's pages restagger for the team, the tile takes the colour and icon, and a highlight glides to whatever the open step changes. It ends on a hand-off to inviting the team.

- Tier: Eased Pro
- Interaction: Type
- Page: https://eased.dev/components/workspace-setup
- Installs: motion, @base-ui/react

## Install

Workspace Setup ships with Eased Pro (https://eased.dev/#pricing). Members add the `@eased-pro` registry with their token (https://eased.dev/account), then run:

```bash
npx shadcn add @eased-pro/workspace-setup
```

A four-step flow that creates a workspace (name, URL, team and look) beside a skeleton of the app that becomes the workspace as you type and choose. Built on Onboarding Shell, the Onboarding core step engine, Field, Input and Base UI's Radio Group.

## When to use

- The first screen after sign-up in a team product: create the workspace before anything else.
- When the workspace has a URL that must be unique, and you want the check to happen as people type.
- When showing the result live is worth more than a shorter form.

## When not to use

- Joining an existing workspace: a list of workspaces with a Join button is enough.
- Settings for a workspace that already exists: a settings page with Field rows.
- A different set of questions: build your own flow on [Onboarding Shell](/components/onboarding-shell).

## Usage

```tsx example.tsx
import { WorkspaceSetup } from "@/components/eased/workspace-setup/workspace-setup";

export function CreateWorkspace() {
  const router = useRouter();
  return (
    <WorkspaceSetup
      className="h-dvh"
      domain="lumen.app"
      checkSlug={(slug, signal) => fetch(`/api/slugs/${slug}`, { signal }).then((r) => r.status === 404)}
      onCreate={(workspace) => api.createWorkspace(workspace)}
      onInvite={(workspace) => router.push(`/${workspace.slug}/invite`)}
      onOpen={(workspace) => router.push(`/${workspace.slug}`)}
    />
  );
}
```

## Examples

### Drive it from outside

`useWorkspaceSetup` returns the controller the component would make. Hold it yourself to prefill, script or test the flow.

```tsx
const setup = useWorkspaceSetup({ domain: "lumen.app", checkSlug, onCreate });
setup.setName("Fernhill");
<WorkspaceSetup setup={setup} onInvite={openInvite} />
```

### Your own teams and colours

```tsx
<WorkspaceSetup
  teams={[{ id: "clinic", label: "Clinic", description: "Patients and rotas", pages: ["Patients", "Rota", "Messages"] }]}
  colors={[{ id: "sage", label: "Sage", value: "oklch(0.55 0.08 150)" }]}
/>
```

## API reference

### WorkspaceSetup

| Prop | Type | Default | Description |
|---|---|---|---|
| `domain` | `string` | `"lumen.app"` | The host the slug hangs off. |
| `checkSlug` | `(slug, signal) => Promise<boolean>` | always free | Resolves true when the slug is free. Called 350ms after the slug settles; the signal aborts when it changes. When taken, it is asked about `-hq`, `-team` and `-app` and offers the first free one. A rejection says the check failed and holds the step. |
| `onCreate` | `(workspace: Workspace) => void \| Promise` | | Runs on Create workspace. A promise shows the button loading; a rejection stays on the last step with its message. |
| `onInvite` | `(workspace) => void` | | The success screen's hand-off. |
| `onOpen` | `(workspace) => void` | | The success screen's quieter way out. |
| `initial` | `Partial<WorkspaceDraft>` | | Prefill, e.g. `{ name: user.company }`. |
| `persist` | `StepsPersist` | off | Save progress across reloads. |
| `teams` | `TeamKind[]` | six teams | `{ id, label, description, pages: [three sidebar pages] }`. |
| `colors` | `WorkspaceColor[]` | eight colours | `{ id, label, value }`. Pick values a white glyph reads on. |
| `setup` | `WorkspaceSetupController` | | From `useWorkspaceSetup`; the props above then go to the hook instead. |
| `manageFocus` | `boolean \| (() => boolean)` | `true` | See Onboarding Shell. |
| `className` | `string` | `""` | Give it a height. |

`Workspace` is `{ name, slug, url, team, color, icon }`. The slug follows the name ("Fernhill Labs" → `fernhill-labs`) until someone edits it; clearing it starts following again.

Also exported: `useWorkspaceSetup`, `WorkspacePreview`, `WorkspaceBadge`, `WorkspaceTile`, `slugify`, `availabilityOf`, `TEAM_KINDS` and `WORKSPACE_COLORS`.

## Keyboard

| Key | Action |
|---|---|
| `Enter` | Continues when the step is valid, from the field or a team card. |
| `Escape` | Goes back a step. |
| Arrow keys | Move between teams, colours and icons. |

## Accessibility

- Each step is a labelled field or radio group; colours and icons are named ("Teal", "Leaf").
- The availability line is part of the field's description, which is a polite live region, so "fernhill is taken" and "lumen.app/fernhill-hq is yours" are read as they settle. The suggestion is a real button.
- The preview is decorative (`aria-hidden`): every value it shows is in the form.
- Focus moves to each step's heading, and to "Fernhill is ready" at the end.

## Motion

The preview is the signature: it mirrors the form with no lag. The name types into the switcher, the tile's colour cross-fades in 200ms and a new icon or first letter swaps with y and blur, the address bar takes the slug, and a new team's pages replace the old in a 35ms stagger. A ring with the field focus glow glides on `smooth` to whatever the open step changes: the switcher's name, the address bar, the team's pages, then the tile. In the form, the availability icon swaps from spinner to check or cross, and the line under the field swaps with it. Reduced motion: values change in place and the ring jumps.

## Notes for AI

Use it as the workspace step of a sign-up for a team product. Wire `checkSlug` to your API with the signal, `onCreate` to the create call, and `onInvite` to your invite flow; that is all a typical app needs.

Don't add your own URL check, debounce or slug derivation; the hook does all three. Don't wrap it in a card or dialog with its own padding: it is a full surface, so give it a height and let it fill.

## Related

- [Onboarding Shell](/components/onboarding-shell): the frame and step engine under this flow.
- [Onboarding Checklist](/components/onboarding-checklist): what comes after setup, inside the product.
