{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"c-stepper-16","type":"registry:block","title":"Stepper with delivery stops and ETAs","description":"A vertical rail of 5 delivery addresses joined by dotted connectors, an On time badge on the current stop and arrival times on hover.","dependencies":["cn"],"registryDependencies":["@reui/badge","button","separator","@reui/stepper","tooltip"],"files":[{"path":"c-stepper-16.tsx","type":"registry:block","content":"\"use client\"\n\nimport { useRef, useState } from \"react\"\nimport { Badge } from \"@/components/reui/badge\"\nimport {\n  Stepper,\n  StepperContent,\n  StepperIndicator,\n  StepperItem,\n  StepperNav,\n  StepperPanel,\n  StepperSeparator,\n  StepperTitle,\n  StepperTrigger,\n} from \"@/components/reui/stepper\"\nimport { cn } from \"cn\"\n\nimport { Button } from \"@/components/ui/button\"\nimport { Separator } from \"@/components/ui/separator\"\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\"\nimport { IconPlaceholder } from \"@/app/(create)/components/icon-placeholder\"\n\nconst stops = [\n  { address: \"Damrak 12\", time: \"09:12\" },\n  { address: \"Rokin 75\", time: \"09:31\" },\n  { address: \"Nieuwezijds 112\", time: \"09:48\" },\n  { address: \"Spuistraat 210\", time: \"10:05\" },\n  { address: \"Haarlemmerstraat 44\", time: \"10:20\" },\n]\n\n// The 14px ring stays transparent and the 10px dot inside it carries the\n// state color, so the 1px gap between them shows whatever surface is behind.\nconst completed = (\n  <span className=\"bg-success flex size-2.5 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\"\n      aria-hidden=\"true\"\n    />\n  </span>\n)\n\nfunction Pending() {\n  return (\n    <span className=\"bg-ring/80 flex size-2.5 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-3.5 items-center justify-center\">\n      <svg\n        viewBox=\"0 0 14 14\"\n        fill=\"none\"\n        strokeWidth=\"1\"\n        aria-hidden=\"true\"\n        className=\"absolute inset-0 size-full\"\n      >\n        <circle\n          cx=\"7\"\n          cy=\"7\"\n          r=\"6\"\n          pathLength={44}\n          strokeDasharray=\"3 1\"\n          className=\"stroke-ring/50\"\n        />\n        <circle\n          cx=\"7\"\n          cy=\"7\"\n          r=\"6\"\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 rounded-full\" />\n    </span>\n  )\n}\n\nconst indicators = {\n  completed,\n  inactive: <Pending />,\n  loading: <Current />,\n}\n\nexport function Pattern() {\n  const [current, setCurrent] = useState(3)\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 > stops.length\n\n  function goTo(step: number) {\n    setCurrent(step)\n    // The pressed button disables itself at either end, so focus moves to\n    // its partner.\n    if (step === 1) nextRef.current?.focus()\n    if (step > stops.length) prevRef.current?.focus()\n  }\n\n  return (\n    <div className=\"@container mx-auto w-full max-w-lg\">\n      <Stepper\n        value={current}\n        onValueChange={setCurrent}\n        orientation=\"vertical\"\n        indicators={indicators}\n        className=\"flex flex-col gap-6 @md:flex-row @md:items-start\"\n        aria-label=\"Delivery route\"\n      >\n        {/* A fixed width, not the natural one: the badge moves with the\n            current stop, so a natural width would shift the content and the\n            buttons under the pointer on every step. */}\n        <StepperNav className=\"w-full max-w-60 shrink-0 gap-2.5\">\n          {stops.map((stop, index) => {\n            const step = index + 1\n            const status =\n              step < current\n                ? `Arrived at ${stop.time}`\n                : step === current\n                  ? \"On time\"\n                  : `Expected at ${stop.time}`\n\n            // The primitive shows loading on the active stop only, so every\n            // stop carries the flag and the spinner stands in for an active\n            // indicator. Each row is 20px (the badge's height) with or without\n            // the badge, so the separators sit at one fixed offset.\n            return (\n              <StepperItem\n                key={stop.address}\n                step={step}\n                loading\n                className=\"relative min-h-5 justify-start gap-1.5 group-data-[orientation=vertical]/stepper-nav:flex-row\"\n              >\n                {/* after: grows the 14px trigger to a 24px hit area without\n                    changing the 20px row the separators are placed on. */}\n                <StepperTrigger\n                  tabIndex={allDone && step === stops.length ? 0 : undefined}\n                  className=\"relative gap-1.5 after:absolute after:inset-x-0 after:-inset-y-1.25\"\n                >\n                  <StepperIndicator className=\"border-ring/80 data-[state=completed]:border-success size-3.5 border bg-transparent data-[state=active]:border-0 data-[state=active]:bg-transparent data-[state=completed]:bg-transparent\" />\n                  <StepperTitle>{stop.address}</StepperTitle>\n                  <span className=\"sr-only\">, {status}</span>\n                </StepperTrigger>\n\n                {/* The tab already announces the status, so the badge and the\n                    info icon are hidden from assistive tech and the icon stays\n                    pointer-only, keeping one tab stop in the tablist. */}\n                {step === current ? (\n                  <Badge\n                    variant=\"success-light\"\n                    radius=\"full\"\n                    aria-hidden=\"true\"\n                  >\n                    {status}\n                  </Badge>\n                ) : (\n                  <Tooltip>\n                    <TooltipTrigger\n                      tabIndex={-1}\n                      aria-hidden=\"true\"\n                      className=\"flex\"\n                    >\n                      <IconPlaceholder\n                        lucide=\"InfoIcon\"\n                        tabler=\"IconInfoCircle\"\n                        hugeicons=\"InformationCircleIcon\"\n                        phosphor=\"InfoIcon\"\n                        remixicon=\"RiInformationLine\"\n                        className=\"text-muted-foreground hover:text-foreground size-3.5\"\n                        aria-hidden=\"true\"\n                      />\n                    </TooltipTrigger>\n                    <TooltipContent>{status}</TooltipContent>\n                  </Tooltip>\n                )}\n\n                {/* Three 3px dots, a gradient cut by a repeating dot mask.\n                    data-state greens the top once this stop is done; the\n                    bottom follows only when the next stop is done too. */}\n                {step < stops.length && (\n                  <StepperSeparator\n                    className={cn(\n                      \"from-ring/80 to-ring/80 data-[state=completed]:from-success pointer-events-none absolute start-1.75 top-4.5 m-0 -translate-x-1/2 bg-transparent bg-linear-to-b to-70% mask-[radial-gradient(circle_closest-side,#000_90%,transparent)] mask-size-[0.1875rem_0.3125rem] mask-center group-data-[orientation=vertical]/stepper-nav:h-3.5 group-data-[orientation=vertical]/stepper-nav:w-0.75 rtl:translate-x-1/2\",\n                      step + 1 < current\n                        ? \"to-success\"\n                        : \"data-[state=completed]:to-ring\"\n                    )}\n                  />\n                )}\n              </StepperItem>\n            )\n          })}\n        </StepperNav>\n\n        {/* Decorative, so hidden from assistive tech: a line across while the\n            columns stack, a line between them once they sit side by side. */}\n        <Separator aria-hidden=\"true\" className=\"@md:hidden\" />\n        <Separator\n          aria-hidden=\"true\"\n          orientation=\"vertical\"\n          className=\"hidden @md:block\"\n        />\n        <div className=\"flex min-w-0 flex-1 flex-col gap-3 text-start\">\n          <StepperPanel>\n            {stops.map((stop, index) => (\n              <StepperContent key={stop.address} value={index + 1}>\n                <p className=\"text-sm font-medium\">{stop.address}</p>\n                <p className=\"text-muted-foreground text-sm\">\n                  Stop {index + 1} of {stops.length}, due at {stop.time}\n                </p>\n              </StepperContent>\n            ))}\n            <StepperContent value={stops.length + 1}>\n              <p className=\"text-sm font-medium\">Route complete</p>\n              <p className=\"text-muted-foreground text-sm\">\n                All {stops.length} stops delivered\n              </p>\n            </StepperContent>\n          </StepperPanel>\n          <div className=\"flex justify-start 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        </div>\n      </Stepper>\n    </div>\n  )\n}","target":"components/examples/c-stepper-16.tsx"}],"meta":{"order":16,"gridSize":1,"label":"Delivery Stops"}}