# Install Guide

> Vertical, non-linear install checklist with a shadcn CLI command in a monospace block under each open step. Done, next ticks the step and moves focus to the following title.

A shadcn/ui block from the [@01orbital](https://01orbital.dev/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/01orbital/install-guide).

- Page, with a live preview: https://shadcn.exchange/r/01orbital/install-guide
- Screenshot: https://shadcn.exchange/shot/01orbital/install-guide/light.png
- Registry item: https://01orbital.dev/r/install-guide.json
- Categories: stepper
- Listed: 2026-11-01, source last changed 2026-11-01

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/01orbital/install-guide.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@phosphor-icons/react`, `cn`
- Registry items: `@01orbital/button`, `@01orbital/font-geist-mono`, `@01orbital/stepper`

## Files

### `blocks/stepper/install-guide/index.tsx`

```tsx
"use client"

import { useEffect, useId, useRef, useState } from "react"
import { CheckIcon } from "@phosphor-icons/react"
import { cn } from "cn"

import {
  Stepper,
  StepperContent,
  StepperDescription,
  StepperIndicator,
  StepperItem,
  StepperList,
  StepperSeparator,
  StepperTitle,
  StepperTrigger,
} from "@/components/stepper/stepper"
import { Button } from "@/components/ui/button"

// The copy: change text here, not in the markup.
const COPY = {
  title: "Install the registry",
  body: "Three commands. Mark each one done as you go.",
  done: "Done, next",
  finished: "All set. Browse the registry to add more.",
  steps: [
    {
      title: "Initialise shadcn",
      body: "Creates components.json and the ui folder.",
      cmd: "pnpm dlx shadcn@latest init",
    },
    {
      title: "Add the registry",
      body: "Points the CLI at Orbcore.",
      cmd: "pnpm dlx shadcn@latest registry add @01orbital",
    },
    {
      title: "Install a block",
      body: "Any block, one command.",
      cmd: "pnpm dlx shadcn@latest add @01orbital/checkout",
    },
  ],
} as const

// Vertical with the command inline under the open step. Non-linear, so a
// reader can jump back to copy a command again.
export default function InstallGuide() {
  const [step, setStep] = useState(0)
  // Done is its own set, so jumping back to copy a command does not undo it.
  const [done, setDone] = useState<number[]>([])
  const id = useId()
  const focusNext = useRef(false)
  // Done, next unmounts the button under the pointer; focus lands on the
  // next step's title instead of falling to the body.
  useEffect(() => {
    if (!focusNext.current) return
    focusNext.current = false
    ;(
      document.getElementById(`${id}step-${step}`) ??
      document.getElementById(`${id}finished`)
    )?.focus()
  }, [id, step])
  const finish = (i: number) => {
    const nextDone = done.includes(i) ? done : [...done, i]
    setDone(nextDone)
    const nextStep = COPY.steps.findIndex(
      (_, index) => !nextDone.includes(index)
    )
    focusNext.current = true
    setStep(nextStep === -1 ? COPY.steps.length : nextStep)
  }
  const finished = done.length === COPY.steps.length
  return (
    <section className="mx-auto w-full max-w-md">
      <h2 className="text-xl font-semibold tracking-tight">{COPY.title}</h2>
      <p className="mt-1 mb-6 text-sm text-muted-foreground">{COPY.body}</p>
      <Stepper value={step} onValueChange={setStep} linear={false}>
        <StepperList className="flex-col items-stretch">
          {COPY.steps.map((s, i) => (
            <StepperItem
              key={s.title}
              value={i}
              className="flex-col items-stretch"
            >
              <div className="flex gap-3">
                <div className="flex flex-col items-center">
                  <StepperIndicator
                    className={cn(
                      "size-6 text-[11px]",
                      done.includes(i) &&
                        "bg-primary text-primary-foreground ring-0"
                    )}
                  >
                    {done.includes(i) ? (
                      <CheckIcon weight="bold" aria-hidden className="size-3" />
                    ) : (
                      <span>{i + 1}</span>
                    )}
                  </StepperIndicator>
                  <StepperSeparator className="mx-0 my-1 h-auto w-px flex-1 group-last/step:hidden" />
                </div>
                <div className="min-w-0 flex-1 pt-0.5 pb-6">
                  <StepperTrigger
                    id={`${id}step-${i}`}
                    className="h-auto justify-start px-0 py-0 text-left hover:bg-transparent"
                  >
                    <StepperTitle>{s.title}</StepperTitle>
                  </StepperTrigger>
                  <StepperContent value={i} className="mt-1 gap-3">
                    <StepperDescription>{s.body}</StepperDescription>
                    <pre className="overflow-x-auto rounded-md bg-muted px-3 py-2 font-mono text-xs leading-6">
                      {s.cmd}
                    </pre>
                    <Button
                      size="sm"
                      className="w-fit"
                      onClick={() => finish(i)}
                    >
                      {COPY.done}
                    </Button>
                  </StepperContent>
                </div>
              </div>
            </StepperItem>
          ))}
        </StepperList>
        {finished && (
          <p
            id={`${id}finished`}
            tabIndex={-1}
            className="text-sm outline-none"
          >
            {COPY.finished}
          </p>
        )}
      </Stepper>
    </section>
  )
}
```
