{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "step-indicator",
  "type": "registry:component",
  "title": "Step Indicator",
  "description": "An animated step indicator with a tooltip that highlights the current progress stage.",
  "dependencies": [
    "motion",
    "react-icons"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "components/watermelon/step-indicator.tsx",
      "type": "registry:component",
      "content": "'use client';\n\nimport { cn } from '@/lib/utils';\nimport { motion, AnimatePresence } from 'motion/react';\nimport { useRef, useState } from 'react';\nimport type { IconType } from 'react-icons';\n\nexport type Step = {\n  id: string;\n  label: string;\n  icon: IconType;\n};\n\ninterface StepIndicatorProps {\n  steps: Step[];\n  tooltipDelay?: number;\n  onStepChange?: (index: number) => void;\n}\n\nexport const StepIndicator = ({\n  steps,\n  tooltipDelay = 0,\n  onStepChange,\n}: StepIndicatorProps) => {\n  const [activeIndex, setActiveIndex] = useState<number | null>(null);\n  const [coords, setCoords] = useState({ clipPath: '', translateX: 0 });\n\n  const measureRefs = useRef<(HTMLDivElement | null)[]>([]);\n  const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]);\n  const [isEntering, setIsEntering] = useState(true);\n  const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  const calculatePosition = (index: number) => {\n    const activeLabel = measureRefs.current[index];\n    const activeButton = buttonRefs.current[index];\n\n    if (!activeLabel || !activeButton) return null;\n\n    const labelLeft = activeLabel.offsetLeft;\n    const labelWidth = activeLabel.offsetWidth;\n    const labelCenter = labelLeft + labelWidth / 2;\n\n    const buttonLeft = activeButton.offsetLeft;\n    const buttonWidth = activeButton.offsetWidth;\n    const buttonCenter = buttonLeft + buttonWidth / 2;\n\n    const totalWidth = measureRefs.current.reduce(\n      (acc, el) => acc + (el?.offsetWidth || 0),\n      0,\n    );\n\n    const cLeft = (labelLeft / totalWidth) * 100;\n    const cRight = 100 - ((labelLeft + labelWidth) / totalWidth) * 100;\n\n    return {\n      clipPath: `inset(0 ${cRight}% 0 ${cLeft}% round 9999px)`,\n      translateX: buttonCenter - labelCenter,\n    };\n  };\n\n  const handleShow = (index: number) => {\n    if (timeoutRef.current) clearTimeout(timeoutRef.current);\n\n    const performUpdate = () => {\n      const newCoords = calculatePosition(index);\n      if (newCoords) {\n        setCoords(newCoords);\n        setActiveIndex(index);\n      }\n    };\n\n    if (activeIndex === null) {\n      setIsEntering(true);\n      if (tooltipDelay > 0) {\n        timeoutRef.current = setTimeout(performUpdate, tooltipDelay);\n      } else {\n        performUpdate();\n      }\n    } else {\n      setIsEntering(false);\n      performUpdate();\n    }\n  };\n\n  const handleHide = () => {\n    if (timeoutRef.current) clearTimeout(timeoutRef.current);\n    setActiveIndex(null);\n    setCoords({ clipPath: '', translateX: 0 });\n    setIsEntering(true);\n  };\n\n  return (\n    <div className=\"flex w-full items-center justify-center px-4 py-20\">\n      <div className=\"w-full max-w-[420px]\">\n        <div\n          className=\"relative flex h-3 w-full items-center gap-3 px-1\"\n          onMouseLeave={handleHide}\n        >\n          <AnimatePresence>\n            {activeIndex !== null && coords.clipPath !== '' && (\n              <motion.div\n                className=\"pointer-events-none absolute bottom-10 left-0\"\n                initial={{ opacity: 0 }}\n                animate={{ opacity: 1 }}\n                exit={{ opacity: 0 }}\n                transition={{ duration: 0.2 }}\n              >\n                <motion.div\n                  className=\"flex bg-black dark:bg-white\"\n                  animate={{\n                    clipPath: coords.clipPath,\n                    x: coords.translateX,\n                  }}\n                  transition={{\n                    type: 'spring',\n                    bounce: 0,\n                    duration: isEntering ? 0 : 0.4,\n                  }}\n                  onUpdate={() => {\n                    if (isEntering) setIsEntering(false);\n                  }}\n                >\n                  <div className=\"inline-flex items-center justify-center\">\n                    {steps.map((step, index) => (\n                      <motion.div\n                        key={`real-${step.id}`}\n                        animate={{\n                          opacity: activeIndex === index ? 1 : 0,\n                          filter:\n                            activeIndex === index ? 'blur(0px)' : 'blur(4px)',\n                        }}\n                        transition={{ duration: 0.4 }}\n                        className=\"flex items-center justify-center gap-2 px-4 py-2 whitespace-nowrap text-white dark:text-black\"\n                      >\n                        <step.icon className=\"size-5\" />\n                        <span className=\"text-lg font-semibold\">\n                          {step.label}\n                        </span>\n                      </motion.div>\n                    ))}\n                  </div>\n                </motion.div>\n              </motion.div>\n            )}\n          </AnimatePresence>\n\n          {steps.map((step, index) => (\n            <button\n              key={step.id}\n              ref={(el) => {\n                buttonRefs.current[index] = el;\n              }}\n              onMouseEnter={() => handleShow(index)}\n              onFocus={() => handleShow(index)}\n              onBlur={(e) => {\n                if (!e.currentTarget.contains(e.relatedTarget as Node)) {\n                  handleHide();\n                }\n              }}\n              onClick={() => onStepChange?.(index)}\n              className=\"group relative h-3 flex-1 cursor-pointer outline-none\"\n            >\n              <div\n                className={cn(\n                  'absolute inset-0 rounded-full bg-zinc-200 transition-colors duration-300 dark:bg-zinc-800',\n                  'group-focus-visible:ring-2 group-focus-visible:ring-zinc-400 group-focus-visible:ring-offset-4 dark:group-focus-visible:ring-offset-zinc-950',\n                  activeIndex === index && 'bg-zinc-800 dark:bg-zinc-100',\n                )}\n              />\n            </button>\n          ))}\n        </div>\n      </div>\n      <div\n        className=\"pointer-events-none absolute bottom-0 left-0 flex h-0 overflow-hidden whitespace-nowrap opacity-0\"\n        aria-hidden=\"true\"\n      >\n        {steps.map((step, index) => (\n          <div\n            key={`measure-${step.id}`}\n            ref={(el) => {\n              measureRefs.current[index] = el;\n            }}\n            className=\"flex items-center justify-center gap-2 px-4 py-2\"\n          >\n            <step.icon className=\"size-5\" />\n            <span className=\"text-lg font-semibold\">{step.label}</span>\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n};\n"
    }
  ]
}
