{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"logo-stepper","type":"registry:ui","title":"Logo Stepper","description":"A row of logos that steps through one at a time, with a spring focus ring sliding between them and a caption per logo.","dependencies":["motion"],"registryDependencies":[],"files":[{"path":"registry/ui/logo-stepper.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useId, useRef, useState, type KeyboardEvent, type ReactNode } from \"react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\n\nexport interface LogoStep {\n  name: string;\n  logo: ReactNode;\n  /** Shown under the row while this logo is highlighted. */\n  caption?: ReactNode;\n}\n\nexport interface LogoStepperProps {\n  logos: LogoStep[];\n  /** Ms each logo stays highlighted. */\n  interval?: number;\n  /** Step automatically. Pauses on hover and focus. */\n  autoPlay?: boolean;\n  /** Focus ring and progress color. */\n  accent?: string;\n  /** Dim and desaturate the logos that are not highlighted. */\n  dimInactive?: boolean;\n  className?: string;\n}\n\nconst css = `@keyframes logo-stepper-progress{from{transform:scaleX(0)}to{transform:scaleX(1)}}`;\n\nexport function LogoStepper({\n  logos,\n  interval = 2400,\n  autoPlay = true,\n  accent = \"#a3e635\",\n  dimInactive = true,\n  className,\n}: LogoStepperProps) {\n  const [active, setActive] = useState(0);\n  const [hovered, setHovered] = useState(false);\n  const [focused, setFocused] = useState(false);\n  const tabs = useRef<(HTMLButtonElement | null)[]>([]);\n  const id = useId();\n  const reduced = useReducedMotion();\n  const n = logos.length;\n  const current = n ? Math.min(active, n - 1) : 0;\n  const playing = autoPlay && !reduced && n > 1;\n  const paused = hovered || focused;\n\n  const select = (i: number, focus = false) => {\n    const next = ((i % n) + n) % n;\n    setActive(next);\n    if (focus) tabs.current[next]?.focus();\n  };\n\n  const onKey = (e: KeyboardEvent) => {\n    if (e.key === \"ArrowRight\") select(current + 1, true);\n    else if (e.key === \"ArrowLeft\") select(current - 1, true);\n    else if (e.key === \"Home\") select(0, true);\n    else if (e.key === \"End\") select(n - 1, true);\n    else return;\n    e.preventDefault();\n  };\n\n  const spring = reduced ? { duration: 0 } : { type: \"spring\" as const, stiffness: 380, damping: 32 };\n  const item = logos[current];\n\n  return (\n    <div\n      className={`flex flex-col items-center ${className ?? \"\"}`}\n      onPointerEnter={() => setHovered(true)}\n      onPointerLeave={() => setHovered(false)}\n      onFocus={() => setFocused(true)}\n      onBlur={(e) => !e.currentTarget.contains(e.relatedTarget) && setFocused(false)}\n    >\n      <style href=\"logo-stepper\" precedence=\"default\">\n        {css}\n      </style>\n      <div role=\"tablist\" aria-label=\"Customers\" onKeyDown={onKey} className=\"flex flex-wrap items-center justify-center gap-2\">\n        {logos.map((l, i) => {\n          const on = i === current;\n          return (\n            <button\n              key={l.name}\n              ref={(el) => {\n                tabs.current[i] = el;\n              }}\n              id={`${id}-tab-${i}`}\n              role=\"tab\"\n              type=\"button\"\n              aria-selected={on}\n              aria-controls={`${id}-panel`}\n              tabIndex={on ? 0 : -1}\n              onClick={() => select(i)}\n              className=\"relative flex h-16 items-center rounded-2xl px-5 outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n            >\n              {on && (\n                <motion.span\n                  layoutId={`${id}-ring`}\n                  transition={spring}\n                  aria-hidden\n                  className=\"absolute inset-0 overflow-hidden border bg-foreground/[0.04]\"\n                  style={{\n                    borderRadius: 16,\n                    borderColor: `color-mix(in oklab, ${accent} 55%, transparent)`,\n                    boxShadow: `0 0 0 4px color-mix(in oklab, ${accent} 12%, transparent), 0 0 32px -6px color-mix(in oklab, ${accent} 45%, transparent)`,\n                  }}\n                >\n                  {playing && (\n                    <span\n                      key={current}\n                      className=\"absolute inset-x-3 bottom-0 h-0.5 origin-left rounded-full\"\n                      style={{\n                        background: accent,\n                        animation: `logo-stepper-progress ${interval}ms linear forwards`,\n                        animationPlayState: paused ? \"paused\" : \"running\",\n                      }}\n                      onAnimationEnd={() => select(current + 1)}\n                    />\n                  )}\n                </motion.span>\n              )}\n              <span\n                className=\"relative flex items-center text-foreground transition-[opacity,filter] duration-500\"\n                style={{ opacity: on || !dimInactive ? 1 : 0.4, filter: on || !dimInactive ? \"none\" : \"grayscale(1)\" }}\n              >\n                {l.logo}\n              </span>\n            </button>\n          );\n        })}\n      </div>\n\n      <div id={`${id}-panel`} role=\"tabpanel\" aria-labelledby={`${id}-tab-${current}`} className=\"mt-8 min-h-14 w-full max-w-xl text-center\">\n        <AnimatePresence mode=\"wait\" initial={false}>\n          {item?.caption && (\n            <motion.div\n              key={current}\n              initial={reduced ? { opacity: 0 } : { opacity: 0, y: 8, filter: \"blur(4px)\" }}\n              animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n              exit={reduced ? { opacity: 0 } : { opacity: 0, y: -8, filter: \"blur(4px)\" }}\n              transition={{ duration: 0.28 }}\n              className=\"text-balance text-lg text-muted-foreground\"\n            >\n              {item.caption}\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </div>\n    </div>\n  );\n}\n"}]}