{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "step-indicator",
  "type": "registry:ui",
  "title": "Step Indicator",
  "description": "A horizontal or vertical wizard stepper whose connectors fill as you advance, with clickable completed steps and a narrow-width summary.",
  "author": "WANSTUDIO <https://wanstudio.my>",
  "dependencies": [],
  "files": [
    {
      "path": "registry/wancraft/step-indicator.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nexport type StepStatus = \"complete\" | \"current\" | \"upcoming\" | \"error\";\n\nexport interface StepIndicatorStep {\n  /** Stable key. Defaults to the index. */\n  id?: string;\n  title: string;\n  description?: React.ReactNode;\n  /** Overrides the status derived from `current` (e.g. \"error\" for a step that failed validation). */\n  status?: StepStatus;\n  /** Marks the step as optional (\"Optional\" label + screen reader text). */\n  optional?: boolean;\n}\n\nexport interface StepIndicatorProps extends Omit<React.HTMLAttributes<HTMLElement>, \"children\"> {\n  steps: StepIndicatorStep[];\n  /** Zero-based index of the current step. Earlier steps are complete, later ones upcoming. */\n  current: number;\n  /** Lay the steps out in a row or a column. */\n  orientation?: \"horizontal\" | \"vertical\";\n  /** Makes steps clickable. Receives the index of the clicked step. */\n  onStepClick?: (index: number) => void;\n  /** Linear mode: only completed steps (and the current one) can be clicked. Otherwise every step can. */\n  linear?: boolean;\n  /** Show each step's description. */\n  showDescriptions?: boolean;\n  /** Time each connector takes to fill, in ms. Several connectors fill one after another. */\n  fillDuration?: number;\n  /** Horizontal only: below this container width the list collapses to \"Step 2 of 5 · Title\". */\n  collapseBelow?: \"none\" | \"24rem\" | \"32rem\" | \"42rem\";\n  /** Fill, current ring and focus colour. */\n  accent?: string;\n  /** Accessible name of the navigation landmark. */\n  label?: string;\n}\n\nfunction subscribeReducedMotion(callback: () => void) {\n  const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n  mq.addEventListener(\"change\", callback);\n  return () => mq.removeEventListener(\"change\", callback);\n}\n\n/** SSR-safe: hydrates as \"no preference\", then follows the media query. */\nfunction usePrefersReducedMotion() {\n  return React.useSyncExternalStore(\n    subscribeReducedMotion,\n    () => window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches,\n    () => false,\n  );\n}\n\n// Container-query class pairs. Written out in full so Tailwind can see every class.\nconst COLLAPSE: Record<NonNullable<StepIndicatorProps[\"collapseBelow\"]>, { list: string; summary: string }> = {\n  none: { list: \"\", summary: \"hidden\" },\n  \"24rem\": { list: \"@max-sm:hidden\", summary: \"hidden @max-sm:block\" },\n  \"32rem\": { list: \"@max-lg:hidden\", summary: \"hidden @max-lg:block\" },\n  \"42rem\": { list: \"@max-2xl:hidden\", summary: \"hidden @max-2xl:block\" },\n};\n\nconst STATUS_TEXT: Record<StepStatus, string> = {\n  complete: \"Completed\",\n  current: \"Current step\",\n  upcoming: \"Not started\",\n  error: \"Needs attention\",\n};\n\n/**\n * A wizard progress indicator: an ordered list of steps whose connectors fill as you advance.\n * Completed steps can be revisited through `onStepClick`; the current step carries aria-current=\"step\".\n */\nexport function StepIndicator({\n  steps,\n  current,\n  orientation = \"horizontal\",\n  onStepClick,\n  linear = true,\n  showDescriptions = true,\n  fillDuration = 450,\n  collapseBelow = \"32rem\",\n  accent = \"#FF7448\",\n  label = \"Progress\",\n  className = \"\",\n  style,\n  ...props\n}: StepIndicatorProps) {\n  const reduceMotion = usePrefersReducedMotion();\n  const total = steps.length;\n  const active = Math.max(0, Math.min(total - 1, Math.round(current)));\n\n  // Remember where the last move started, so connectors fill (or drain) one after another.\n  const [shown, setShown] = React.useState(active);\n  const [from, setFrom] = React.useState(active);\n  if (active !== shown) {\n    setFrom(shown);\n    setShown(active);\n  }\n\n  const duration = reduceMotion ? 0 : fillDuration;\n  const statusOf = (index: number): StepStatus =>\n    steps[index]?.status ?? (index < active ? \"complete\" : index === active ? \"current\" : \"upcoming\");\n  const canClick = (index: number) => !!onStepClick && (!linear || index <= active);\n\n  // Delay for the connector after step `index`: forward moves fill left-to-right, backward moves drain right-to-left.\n  const connectorDelay = (index: number) => {\n    if (active > from && index >= from && index < active) return (index - from) * duration;\n    if (active < from && index >= active && index < from) return (from - 1 - index) * duration;\n    return 0;\n  };\n\n  const vertical = orientation === \"vertical\";\n  const collapse = vertical ? COLLAPSE.none : COLLAPSE[collapseBelow];\n  const currentStep = steps[active];\n\n  return (\n    <nav\n      aria-label={label}\n      className={`@container w-full text-zinc-900 dark:text-zinc-100 ${className}`}\n      style={{ [\"--si-accent\" as string]: accent, ...style }}\n      {...props}\n    >\n      <style>{`@keyframes si-draw{from{stroke-dashoffset:1}to{stroke-dashoffset:0}}`}</style>\n      <ol className={`${vertical ? \"flex flex-col\" : \"flex items-start\"} ${collapse.list}`}>\n        {steps.map((step, index) => {\n          const status = statusOf(index);\n          const last = index === total - 1;\n          const filled = index < active;\n          const clickable = canClick(index);\n          const Tag = clickable ? \"button\" : \"div\";\n\n          const marker = <StepMarker status={status} index={index} duration={duration} />;\n          const connector = !last && (\n            <span\n              aria-hidden=\"true\"\n              className={`relative block overflow-hidden rounded-full bg-zinc-900/10 dark:bg-white/10 ${\n                vertical ? \"my-1.5 w-[2px] flex-1\" : \"mx-2 h-[2px] flex-1\"\n              }`}\n            >\n              <span\n                className={`absolute inset-0 bg-[var(--si-accent)] transition-transform ease-[cubic-bezier(0.65,0,0.35,1)] ${\n                  vertical ? \"origin-top\" : \"origin-left\"\n                }`}\n                style={{\n                  transform: vertical ? `scaleY(${filled ? 1 : 0})` : `scaleX(${filled ? 1 : 0})`,\n                  transitionDuration: `${duration}ms`,\n                  transitionDelay: `${connectorDelay(index)}ms`,\n                }}\n              />\n            </span>\n          );\n\n          const text = (\n            <span className={`flex min-w-0 flex-col ${vertical ? \"pt-1\" : \"mt-2.5 pr-3\"}`}>\n              <span\n                className={`text-[13px] font-medium leading-tight tracking-tight transition-colors ${\n                  status === \"upcoming\"\n                    ? \"text-zinc-500 dark:text-zinc-400\"\n                    : status === \"error\"\n                      ? \"text-red-600 dark:text-red-400\"\n                      : \"text-zinc-900 dark:text-zinc-50\"\n                }`}\n              >\n                {step.title}\n              </span>\n              {step.optional && (\n                <span className=\"mt-1 font-mono text-[9px] uppercase tracking-[0.14em] text-zinc-400 dark:text-zinc-500\">Optional</span>\n              )}\n              {showDescriptions && step.description && (\n                <span className=\"mt-1 text-[12px] leading-snug text-zinc-500 dark:text-zinc-400\">{step.description}</span>\n              )}\n              <span className=\"sr-only\">\n                , {STATUS_TEXT[status]}\n                {step.optional ? \", optional\" : \"\"}\n              </span>\n            </span>\n          );\n\n          return (\n            <li\n              key={step.id ?? index}\n              className={vertical ? \"flex gap-3\" : `flex min-w-0 flex-col ${last ? \"shrink-0\" : \"flex-1\"}`}\n            >\n              {vertical ? (\n                <>\n                  <span className=\"flex flex-col items-center\">\n                    <Tag\n                      {...(clickable ? { type: \"button\" as const, onClick: () => onStepClick?.(index) } : {})}\n                      aria-current={index === active ? \"step\" : undefined}\n                      aria-label={clickable ? `${step.title}, ${STATUS_TEXT[status]}${step.optional ? \", optional\" : \"\"}` : undefined}\n                      className={`group/step rounded-full ${clickable ? \"cursor-pointer outline-hidden focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-[var(--si-accent)]\" : \"\"}`}\n                    >\n                      {marker}\n                    </Tag>\n                    {connector}\n                  </span>\n                  <span className={last ? \"\" : \"pb-6\"}>{text}</span>\n                </>\n              ) : (\n                <Tag\n                  {...(clickable ? { type: \"button\" as const, onClick: () => onStepClick?.(index) } : {})}\n                  aria-current={index === active ? \"step\" : undefined}\n                  className={`group/step flex min-w-0 flex-col items-stretch rounded-lg text-left ${\n                    clickable\n                      ? \"cursor-pointer outline-hidden focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-solid focus-visible:outline-[var(--si-accent)]\"\n                      : \"\"\n                  }`}\n                >\n                  <span className=\"flex w-full items-center\">\n                    {marker}\n                    {connector}\n                  </span>\n                  {text}\n                </Tag>\n              )}\n            </li>\n          );\n        })}\n      </ol>\n\n      {/* Collapsed summary for narrow containers (horizontal only). */}\n      {currentStep && (\n        <div className={collapse.summary}>\n          <p className=\"flex min-w-0 items-baseline gap-2 text-[13px]\">\n            <span className=\"shrink-0 font-mono text-[10px] uppercase tracking-[0.14em] text-zinc-500 dark:text-zinc-400\">\n              Step {active + 1} of {total}\n            </span>\n            <span aria-hidden=\"true\" className=\"text-zinc-300 dark:text-zinc-600\">\n              ·\n            </span>\n            <span\n              aria-current=\"step\"\n              className={`truncate font-medium tracking-tight ${statusOf(active) === \"error\" ? \"text-red-600 dark:text-red-400\" : \"\"}`}\n            >\n              {currentStep.title}\n            </span>\n          </p>\n          <div aria-hidden=\"true\" className=\"mt-2.5 flex gap-1\">\n            {steps.map((step, index) => {\n              const status = statusOf(index);\n              return (\n                <span key={step.id ?? index} className=\"relative h-1 flex-1 overflow-hidden rounded-full bg-zinc-900/10 dark:bg-white/10\">\n                  <span\n                    className={`absolute inset-0 origin-left transition-transform ease-[cubic-bezier(0.65,0,0.35,1)] ${\n                      status === \"error\" ? \"bg-red-500\" : \"bg-[var(--si-accent)]\"\n                    }`}\n                    style={{\n                      transform: `scaleX(${status === \"complete\" || status === \"error\" ? 1 : status === \"current\" ? 0.5 : 0})`,\n                      transitionDuration: `${duration}ms`,\n                    }}\n                  />\n                </span>\n              );\n            })}\n          </div>\n        </div>\n      )}\n    </nav>\n  );\n}\n\nfunction StepMarker({ status, index, duration }: { status: StepStatus; index: number; duration: number }) {\n  const base =\n    \"relative flex size-7 shrink-0 items-center justify-center rounded-full border font-mono text-[11px] tabular-nums transition-[background-color,border-color,color,box-shadow] duration-300\";\n  const tone = {\n    complete: \"border-[var(--si-accent)] bg-[var(--si-accent)] text-zinc-950 group-hover/step:brightness-95\",\n    current:\n      \"border-[var(--si-accent)] bg-white text-zinc-900 shadow-[0_0_0_4px_color-mix(in_oklab,var(--si-accent)_22%,transparent)] dark:bg-zinc-950 dark:text-zinc-50\",\n    upcoming:\n      \"border-zinc-900/15 bg-white text-zinc-500 group-hover/step:border-zinc-900/30 dark:border-white/15 dark:bg-zinc-950 dark:text-zinc-400 dark:group-hover/step:border-white/30\",\n    error: \"border-red-500 bg-red-500 text-white shadow-[0_0_0_4px_rgba(239,68,68,0.18)]\",\n  }[status];\n\n  return (\n    <span aria-hidden=\"true\" className={`${base} ${tone}`}>\n      {status === \"complete\" ? (\n        <svg viewBox=\"0 0 16 16\" className=\"size-3.5\" fill=\"none\">\n          <path\n            d=\"M3.5 8.5 6.5 11.5 12.5 4.5\"\n            stroke=\"currentColor\"\n            strokeWidth={2}\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            pathLength={1}\n            // Draws the check in once the step completes.\n            className=\"[stroke-dasharray:1] [animation:si-draw_var(--si-draw)_ease-out_both]\"\n            style={{ [\"--si-draw\" as string]: `${Math.max(1, duration)}ms` }}\n          />\n        </svg>\n      ) : status === \"error\" ? (\n        <svg viewBox=\"0 0 16 16\" className=\"size-3.5\" fill=\"none\">\n          <path d=\"M8 4v5\" stroke=\"currentColor\" strokeWidth={2} strokeLinecap=\"round\" />\n          <circle cx=\"8\" cy=\"11.75\" r=\"1.1\" fill=\"currentColor\" />\n        </svg>\n      ) : (\n        <span className={status === \"current\" ? \"font-semibold\" : \"\"}>{String(index + 1).padStart(2, \"0\")}</span>\n      )}\n    </span>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "navigation"
  ],
  "docs": "Documentation: https://wancraft.my/components/step-indicator"
}
