{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"stepper","title":"Stepper","description":"Ordered workflow progress with read-only and native button items.","dependencies":["class-variance-authority@0.7.1","lucide-react@1.18.0"],"registryDependencies":["@siteplane/base","@siteplane/button","@siteplane/motion"],"files":[{"path":"packages/ui/src/components/stepper/stepper.tsx","content":"import { cva, type VariantProps } from \"class-variance-authority\";\nimport { Check, Lock } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport { cn } from \"../../system/utils/cn\";\nimport { slotClass } from \"../../system/utils/slot-class\";\nimport { KitButton } from \"../button/button-internal\";\nimport { MotionReveal, MotionSwap } from \"../motion\";\n\nexport function Stepper({\n  className,\n  ...props\n}: React.ComponentProps<\"ol\">): React.ReactElement {\n  return (\n    <ol\n      className={cn(slotClass(\"stepper\", props), \"flex flex-col\", className)}\n      data-slot=\"stepper\"\n      {...props}\n    />\n  );\n}\n\n// A step that changes status eases its colours instead of switching them. The\n// marker's edge, ring and transitions are design-rework.css's, so the reduced\n// motion modes switch them off.\nconst stepIndicatorVariants = cva(\n  \"relative z-10 flex shrink-0 items-center justify-center rounded-full font-(--sp-rw-font-weight-strong) after:pointer-events-none after:absolute after:-inset-1 after:origin-center after:scale-75 after:rounded-full after:opacity-0 after:content-['']\",\n  {\n    defaultVariants: { status: \"open\" },\n    variants: {\n      status: {\n        blocked: \"border-dashed\",\n        current: \"\",\n        done: \"\",\n        open: \"\",\n      },\n    },\n  },\n);\n\ntype StepperItemStatus = VariantProps<typeof stepIndicatorVariants>[\"status\"];\n\n// Done and blocked markers are icons only, so the title carries their status\n// as visually hidden text.\nconst defaultStatusLabels: Partial<\n  Record<NonNullable<StepperItemStatus>, string>\n> = {\n  blocked: \"blocked\",\n  done: \"completed\",\n};\n\n// A step is the marker and its text, then the body, with the connector to the\n// next step running down under the marker, at the step's inline start: on the\n// right in a right-to-left page. The body opens and closes inside the step, so\n// a step keeps its marker, title and connector when its content changes.\nconst stepItemClassName =\n  \"relative pb-5 last:pb-0 before:absolute before:top-7 before:bottom-0 before:start-3 before:-translate-x-1/2 rtl:before:translate-x-1/2 last:before:hidden\";\n\nexport interface StepperItemProps\n  extends\n    Omit<React.ComponentProps<\"li\">, \"title\">,\n    VariantProps<typeof stepIndicatorVariants> {\n  title: React.ReactNode;\n  /**\n   * Supporting text under the title. Text that changes cross-fades; give a\n   * description element a `key` to cross-fade between elements, or keep the\n   * same `key` to update it in place.\n   */\n  description?: React.ReactNode;\n  index?: number;\n  statusLabel?: string;\n  /**\n   * The content of the step, shown under its text. It expands when children\n   * appear and collapses when they go away, so keep the step mounted between\n   * renders and pass children only while the step shows its content.\n   */\n  children?: React.ReactNode;\n}\n\nexport function StepperItem({\n  className,\n  status = \"open\",\n  title,\n  description,\n  index,\n  statusLabel,\n  children,\n  ...props\n}: StepperItemProps): React.ReactElement {\n  return (\n    <li\n      aria-current={status === \"current\" ? \"step\" : undefined}\n      className={cn(\n        slotClass(\"stepper-item\", props),\n        stepItemClassName,\n        className,\n      )}\n      data-slot=\"stepper-item\"\n      data-status={status}\n      {...props}\n    >\n      <div className=\"flex gap-3\">\n        <StepperIndicator index={index} status={status} />\n        <StepperContent\n          description={description}\n          status={status}\n          statusLabel={statusLabel}\n          title={title}\n        />\n      </div>\n      <StepperBody>{children}</StepperBody>\n    </li>\n  );\n}\n\nexport interface StepperButtonItemProps\n  extends\n    Omit<React.ComponentProps<\"button\">, \"title\">,\n    VariantProps<typeof stepIndicatorVariants> {\n  title: React.ReactNode;\n  /** See `StepperItem`: text that changes cross-fades. */\n  description?: React.ReactNode;\n  index?: number;\n  /**\n   * Marks the step that is open (`aria-current=\"step\"`), independent of its\n   * `status`.\n   */\n  active?: boolean;\n  /** @deprecated Use `active`. */\n  current?: boolean;\n  statusLabel?: string;\n  /**\n   * The content of the step, shown under its text and outside the button, so\n   * it can hold fields and links. It expands when children appear and\n   * collapses when they go away: one item can be the clickable header of a\n   * step and open as the current step, without being replaced.\n   */\n  children?: React.ReactNode;\n}\n\nexport function StepperButtonItem({\n  className,\n  status = \"open\",\n  title,\n  description,\n  index,\n  active: activeProp,\n  current,\n  statusLabel,\n  children,\n  ...props\n}: StepperButtonItemProps): React.ReactElement {\n  const active = activeProp ?? current ?? false;\n  // The title and its status name the button; the marker number and the\n  // description would otherwise run into one long name.\n  const titleId = React.useId();\n  const descriptionId = React.useId();\n  return (\n    <li\n      className={cn(\"sp-stepper-item\", stepItemClassName)}\n      data-slot=\"stepper-item\"\n      data-status={status}\n    >\n      <KitButton\n        aria-current={active ? \"step\" : undefined}\n        aria-describedby={description ? descriptionId : undefined}\n        aria-labelledby={titleId}\n        className={cn(\n          \"[&:not(:disabled):not([aria-current=step]):hover_[data-slot=stepper-indicator]]:after:scale-100 [&:not(:disabled):not([aria-current=step]):hover_[data-slot=stepper-indicator]]:after:opacity-100\",\n          \"[&[aria-current=step]_[data-slot=stepper-indicator]]:after:scale-100 [&[aria-current=step]_[data-slot=stepper-indicator]]:after:opacity-100\",\n          className,\n        )}\n        data-slot=\"stepper-button-item\"\n        profile=\"step-trigger\"\n        variant=\"text\"\n        {...props}\n      >\n        <StepperIndicator index={index} status={status} />\n        <StepperContent\n          description={description}\n          descriptionId={descriptionId}\n          status={status}\n          statusLabel={statusLabel}\n          title={title}\n          titleId={titleId}\n        />\n      </KitButton>\n      <StepperBody>{children}</StepperBody>\n    </li>\n  );\n}\n\n/**\n * The marker's content: a number, or a check or lock for a done or blocked\n * step. A new content cross-fades in the same place.\n */\nfunction StepperIndicator({\n  index,\n  status,\n}: {\n  index?: number | undefined;\n  status: StepperItemStatus;\n}) {\n  const content =\n    status === \"done\" ? (\n      <Check />\n    ) : status === \"blocked\" ? (\n      <Lock />\n    ) : index != null ? (\n      index\n    ) : null;\n  const contentKey =\n    status === \"done\" || status === \"blocked\" ? status : `number:${index}`;\n\n  return (\n    <span\n      className={cn(\"sp-stepper-indicator\", stepIndicatorVariants({ status }))}\n      data-slot=\"stepper-indicator\"\n    >\n      <MotionSwap\n        className=\"grid size-full place-items-center\"\n        swapKey={contentKey}\n      >\n        {content}\n      </MotionSwap>\n    </span>\n  );\n}\n\n/**\n * What identifies a description for the swap: text by its value, an element by\n * its `key` or else its type.\n */\nfunction descriptionKey(description: React.ReactNode): React.Key {\n  if (typeof description === \"string\" || typeof description === \"number\")\n    return `text:${description}`;\n  if (React.isValidElement(description))\n    return (\n      description.key ??\n      `type:${typeof description.type === \"string\" ? description.type : \"component\"}`\n    );\n  return \"none\";\n}\n\nfunction StepperContent({\n  description,\n  descriptionId,\n  status,\n  statusLabel,\n  title,\n  titleId,\n}: {\n  description?: React.ReactNode;\n  descriptionId?: string | undefined;\n  status: StepperItemStatus;\n  statusLabel?: string | undefined;\n  title: React.ReactNode;\n  titleId?: string | undefined;\n}) {\n  const hiddenStatus =\n    statusLabel ?? (status ? defaultStatusLabels[status] : undefined);\n\n  return (\n    <div className=\"min-w-0 flex-1 pt-0.5\">\n      <div\n        className=\"sp-stepper-title wrap-anywhere\"\n        data-slot=\"stepper-title\"\n        id={titleId}\n      >\n        {title}\n        {hiddenStatus ? (\n          <span\n            className=\"sp-stepper-status sr-only\"\n            data-slot=\"stepper-status\"\n          >\n            {` (${hiddenStatus})`}\n          </span>\n        ) : null}\n      </div>\n      <MotionReveal open={Boolean(description)}>\n        <div\n          className=\"sp-stepper-description mt-0.5 wrap-anywhere\"\n          data-slot=\"stepper-description\"\n          id={descriptionId}\n        >\n          <MotionSwap swapKey={descriptionKey(description)}>\n            {description}\n          </MotionSwap>\n        </div>\n      </MotionReveal>\n    </div>\n  );\n}\n\n/**\n * The step's content under its text, in line with the title. The 12 px above\n * it are part of the body, so they open and close with it.\n */\nfunction StepperBody({ children }: { children?: React.ReactNode }) {\n  return (\n    <MotionReveal open={React.Children.toArray(children).length > 0}>\n      <div className=\"sp-stepper-body ps-9 pt-3\" data-slot=\"stepper-body\">\n        {children}\n      </div>\n    </MotionReveal>\n  );\n}\n","type":"registry:ui","target":"~/src/siteplane/components/stepper/stepper.tsx"},{"path":"packages/ui/src/components/stepper/index.ts","content":"export * from \"./stepper\";\n","type":"registry:ui","target":"~/src/siteplane/components/stepper/index.ts"}],"type":"registry:ui","docs":"https://ui.siteplane.io/docs-markdown/components/stepper.md","categories":["Data display"],"meta":{"avoidWhen":["Avoid for panels that people switch between freely; use Tabs.","Avoid for a plain percentage of completion; use Progress."],"description":"Ordered workflow progress with read-only and native button items.","knownLimitations":["Keep one item per step mounted between renders, with a stable key. The marker, the description and the connector then change in place and the content opens and closes; replacing one item component with another remounts the step.","A step's content is the children of its item. A body that is closing is inert, so focus inside it is lost when it closes; move focus first."],"providerNotes":["Depends on the motion component, which the registry installs with the stepper.","SiteplaneMotionProvider preset=\"reduced\" and the system setting make every change instant."],"releaseStatus":"stable","searchTerms":["stepper"],"useWhen":["Use to show ordered steps and which are done, current or blocked, with optional content for the open step."]}}