{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"c-stepper-17","type":"registry:block","title":"Stepper with dotted progress","description":"5 label-free dots for a workspace setup flow, with the step count, name and hint of the selected step shown below.","dependencies":["cn"],"registryDependencies":["button","@reui/stepper"],"files":[{"path":"c-stepper-17.tsx","type":"registry:block","content":"\"use client\"\n\nimport { useRef, useState } from \"react\"\nimport {\n  Stepper,\n  StepperContent,\n  StepperIndicator,\n  StepperItem,\n  StepperNav,\n  StepperPanel,\n  StepperSeparator,\n  StepperTrigger,\n} from \"@/components/reui/stepper\"\nimport { cn } from \"cn\"\n\nimport { Button } from \"@/components/ui/button\"\nimport { IconPlaceholder } from \"@/app/(create)/components/icon-placeholder\"\n\nconst steps = [\n  { title: \"Account\", description: \"Create your login\" },\n  { title: \"Workspace\", description: \"Name your workspace\" },\n  { title: \"Team\", description: \"Invite your teammates\" },\n  { title: \"Billing\", description: \"Choose a plan\" },\n  { title: \"Review\", description: \"Confirm and launch\" },\n]\n\nfunction Pending() {\n  return (\n    <span className=\"bg-ring/80 flex size-3 items-center justify-center rounded-full text-white\">\n      <IconPlaceholder\n        lucide=\"CircleDashedIcon\"\n        tabler=\"IconCircleDashed\"\n        hugeicons=\"CircleDashedIcon\"\n        phosphor=\"CircleDashedIcon\"\n        remixicon=\"RiCheckboxBlankCircleLine\"\n        className=\"size-2\"\n        aria-hidden=\"true\"\n      />\n    </span>\n  )\n}\n\n// The current step trades its solid ring for a dashed track with a 2px green\n// arc spinning over it, inset so the arc stays inside the box and 1px clear\n// of the dot. pathLength={44} counts the ring as 44 units, so the track closes\n// in eleven 3+1 dashes at any size; only the arc moves.\nfunction Current() {\n  return (\n    <span className=\"relative flex size-4 items-center justify-center\">\n      <svg\n        viewBox=\"0 0 16 16\"\n        fill=\"none\"\n        strokeWidth=\"1\"\n        aria-hidden=\"true\"\n        className=\"absolute inset-0 size-full\"\n      >\n        <circle\n          cx=\"8\"\n          cy=\"8\"\n          r=\"7\"\n          pathLength={44}\n          strokeDasharray=\"3 1\"\n          className=\"stroke-ring/50\"\n        />\n        <circle\n          cx=\"8\"\n          cy=\"8\"\n          r=\"7\"\n          pathLength={44}\n          strokeWidth=\"2\"\n          strokeLinecap=\"round\"\n          strokeDasharray=\"15 29\"\n          className=\"stroke-success origin-center motion-safe:animate-spin\"\n        />\n      </svg>\n      <span className=\"bg-ring/80 size-2.5 rounded-full\" />\n    </span>\n  )\n}\n\n// The 16px ring keeps a transparent fill and the 12px dot inside it carries\n// the state color, so the 1px gap between them shows whatever surface is behind.\nconst indicators = {\n  completed: (\n    <span className=\"bg-success flex size-3 items-center justify-center rounded-full text-white\">\n      <IconPlaceholder\n        lucide=\"CheckIcon\"\n        tabler=\"IconCheck\"\n        hugeicons=\"Tick02Icon\"\n        phosphor=\"CheckIcon\"\n        remixicon=\"RiCheckLine\"\n        className=\"size-2.5\"\n        aria-hidden=\"true\"\n      />\n    </span>\n  ),\n  inactive: <Pending />,\n  loading: <Current />,\n}\n\nexport function Pattern() {\n  const [current, setCurrent] = useState(4)\n  const prevRef = useRef<HTMLButtonElement>(null)\n  const nextRef = useRef<HTMLButtonElement>(null)\n  // One past the last step marks every step complete. No tab is selected\n  // then, so the last one keeps the tablist's tab stop.\n  const allDone = current > steps.length\n\n  function goTo(step: number) {\n    setCurrent(step)\n    // The pressed button disables itself at either end, so focus moves to its partner.\n    if (step === 1) nextRef.current?.focus()\n    if (step > steps.length) prevRef.current?.focus()\n  }\n\n  return (\n    <Stepper\n      value={current}\n      onValueChange={setCurrent}\n      indicators={indicators}\n      className=\"max-w-60 space-y-6\"\n      aria-label=\"Workspace setup\"\n    >\n      <StepperNav>\n        {steps.map((item, index) => {\n          const step = index + 1\n\n          // The primitive shows loading on the active step only, so every\n          // step carries the flag, the spinner follows the selection and\n          // `indicators` needs no `active` entry.\n          return (\n            <StepperItem key={item.title} step={step} loading>\n              {/* after: grows the 16px dot to a 24px hit area without\n                  moving the connectors. */}\n              <StepperTrigger\n                tabIndex={allDone && step === steps.length ? 0 : undefined}\n                aria-label={`Step ${step}: ${item.title}`}\n                className=\"relative after:absolute after:-inset-1\"\n              >\n                <StepperIndicator className=\"border-ring/80 data-[state=completed]:border-success size-4 border bg-transparent data-[state=active]:border-0 data-[state=active]:bg-transparent data-[state=completed]:bg-transparent\" />\n              </StepperTrigger>\n\n              {/* 3px dots spaced edge to edge by mask-repeat: space, so a\n                  connector at least one tile wide never shows a clipped dot.\n                  data-state greens the start once this step is done; the end\n                  follows only when the next step is done too. */}\n              {step < steps.length && (\n                <StepperSeparator\n                  className={cn(\n                    \"from-ring/80 to-ring/80 data-[state=completed]:from-success bg-transparent bg-linear-to-r to-70% mask-[radial-gradient(circle_closest-side,#000_90%,transparent)] mask-size-[0.375rem_0.1875rem] mask-repeat-space group-data-[orientation=horizontal]/stepper-nav:h-0.75 rtl:bg-linear-to-l\",\n                    step + 1 < current\n                      ? \"to-success\"\n                      : \"data-[state=completed]:to-ring\"\n                  )}\n                />\n              )}\n            </StepperItem>\n          )\n        })}\n      </StepperNav>\n\n      <StepperPanel className=\"text-center\">\n        {steps.map((item, index) => (\n          <StepperContent key={item.title} value={index + 1}>\n            <p className=\"text-muted-foreground text-xs\">\n              Step {index + 1} of {steps.length}\n            </p>\n            <p className=\"text-sm font-medium\">{item.title}</p>\n            <p className=\"text-muted-foreground text-sm\">{item.description}</p>\n          </StepperContent>\n        ))}\n        <StepperContent value={steps.length + 1}>\n          <p className=\"text-muted-foreground text-xs\">\n            All {steps.length} steps done\n          </p>\n          <p className=\"text-sm font-medium\">Workspace ready</p>\n          <p className=\"text-muted-foreground text-sm\">\n            Setup is complete and your team can sign in.\n          </p>\n        </StepperContent>\n        <div className=\"mt-3 flex justify-center gap-2\">\n          <Button\n            ref={prevRef}\n            variant=\"outline\"\n            size=\"sm\"\n            disabled={current === 1}\n            onClick={() => goTo(current - 1)}\n          >\n            Previous\n          </Button>\n          <Button\n            ref={nextRef}\n            variant=\"outline\"\n            size=\"sm\"\n            disabled={allDone}\n            onClick={() => goTo(current + 1)}\n          >\n            Next\n          </Button>\n        </div>\n      </StepperPanel>\n    </Stepper>\n  )\n}","target":"components/examples/c-stepper-17.tsx"}],"meta":{"order":17,"gridSize":1,"label":"Dotted Progress"}}