{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"feature-showcase","type":"registry:ui","title":"Feature Showcase","description":"Feature list with an expanding description and a filling progress bar that auto-advances, crossfading a matching visual on the side.","dependencies":[],"registryDependencies":["utils"],"files":[{"path":"registry/ui/feature-showcase.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useId, useRef, useState, type CSSProperties, type KeyboardEvent, type ReactNode } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n// The progress bar is a CSS animation whose `animationend` advances the showcase,\n// so pausing is just `animation-play-state` (hover / keyboard focus) and there are\n// no timers to keep in sync. Reduced motion disables the bar and with it auto-advance.\nconst css = `\n@keyframes fs-fill{from{transform:scaleX(0)}to{transform:scaleX(1)}}\n.fs-bar{transform-origin:left;animation:fs-fill var(--fs-dur) linear forwards}\n.fs-root:is(:hover,:focus-within)[data-pause] .fs-bar{animation-play-state:paused}\n@media (prefers-reduced-motion:reduce){.fs-bar{animation:none;transform:scaleX(1)}.fs-visual{transition:none!important}}\n`;\n\nexport interface Feature {\n  title: string;\n  description: ReactNode;\n  icon?: ReactNode;\n  /** Shown on the right while this feature is active. */\n  visual: ReactNode;\n}\n\nexport interface FeatureShowcaseProps {\n  features: Feature[];\n  /** Time each feature stays active before advancing, in ms. */\n  interval?: number;\n  /** Advance to the next feature automatically. */\n  autoPlay?: boolean;\n  /** Pause the timer while the pointer or keyboard focus is inside. */\n  pauseOnHover?: boolean;\n  /** Put the visual on the left instead. */\n  reverse?: boolean;\n  defaultIndex?: number;\n  onChange?: (index: number) => void;\n  className?: string;\n}\n\nexport function FeatureShowcase({\n  features,\n  interval = 5000,\n  autoPlay = true,\n  pauseOnHover = true,\n  reverse = false,\n  defaultIndex = 0,\n  onChange,\n  className,\n}: FeatureShowcaseProps) {\n  const [active, setActive] = useState(defaultIndex);\n  const [cycle, setCycle] = useState(0); // restarts the bar when re-selecting\n  const tabs = useRef<(HTMLButtonElement | null)[]>([]);\n  const id = useId();\n  const n = features.length;\n\n  const select = (i: number, focus = false) => {\n    const next = (i + n) % n;\n    setActive(next);\n    setCycle((c) => c + 1);\n    onChange?.(next);\n    if (focus) tabs.current[next]?.focus();\n  };\n\n  const onKey = (e: KeyboardEvent) => {\n    const map: Record<string, number> = { ArrowDown: active + 1, ArrowRight: active + 1, ArrowUp: active - 1, ArrowLeft: active - 1, Home: 0, End: n - 1 };\n    if (e.key in map) {\n      e.preventDefault();\n      select(map[e.key], true);\n    }\n  };\n\n  return (\n    <div\n      data-pause={pauseOnHover || undefined}\n      className={cn(\"fs-root grid w-full max-w-5xl items-center gap-6 md:grid-cols-[minmax(0,5fr)_minmax(0,6fr)] md:gap-10\", className)}\n      style={{ \"--fs-dur\": `${interval}ms` } as CSSProperties}\n    >\n      <style href=\"feature-showcase\" precedence=\"default\">\n        {css}\n      </style>\n\n      <div role=\"tablist\" aria-orientation=\"vertical\" aria-label=\"Features\" onKeyDown={onKey} className={cn(\"grid gap-1.5\", reverse && \"md:order-2\")}>\n        {features.map((f, i) => {\n          const on = i === active;\n          return (\n            <button\n              key={f.title}\n              ref={(el) => {\n                tabs.current[i] = el;\n              }}\n              type=\"button\"\n              role=\"tab\"\n              id={`${id}-t${i}`}\n              aria-selected={on}\n              aria-controls={`${id}-p`}\n              tabIndex={on ? 0 : -1}\n              onClick={() => select(i)}\n              className={cn(\n                \"group relative overflow-hidden rounded-xl border px-4 py-3.5 text-left outline-none transition-[background-color,border-color] duration-300 focus-visible:ring-2 focus-visible:ring-ring/60\",\n                on ? \"border-border bg-card shadow-sm\" : \"border-transparent hover:bg-muted/50\",\n              )}\n            >\n              <span className=\"flex items-center gap-3\">\n                {f.icon && (\n                  <span\n                    className={cn(\n                      \"grid size-8 shrink-0 place-items-center rounded-lg transition-colors duration-300 [&_svg]:size-4\",\n                      on ? \"bg-primary text-primary-foreground\" : \"bg-muted text-muted-foreground group-hover:text-foreground\",\n                    )}\n                  >\n                    {f.icon}\n                  </span>\n                )}\n                <span className={cn(\"font-medium transition-colors\", on ? \"text-foreground\" : \"text-muted-foreground group-hover:text-foreground\")}>{f.title}</span>\n              </span>\n              {/* grid-rows 0fr -> 1fr animates to the description's natural height */}\n              <span\n                className={cn(\n                  \"grid transition-[grid-template-rows,opacity] duration-300 ease-out motion-reduce:transition-none\",\n                  on ? \"grid-rows-[1fr] opacity-100\" : \"grid-rows-[0fr] opacity-0\",\n                )}\n              >\n                <span className={cn(\"overflow-hidden text-sm leading-relaxed text-muted-foreground\", f.icon && \"pl-11\")}>\n                  <span className=\"block pt-1.5\">{f.description}</span>\n                </span>\n              </span>\n              {on && (\n                <span aria-hidden className=\"absolute inset-x-0 bottom-0 h-0.5 bg-muted\">\n                  <span\n                    key={cycle}\n                    className=\"fs-bar block h-full bg-primary\"\n                    style={autoPlay ? undefined : { animation: \"none\", transform: \"scaleX(1)\" }}\n                    onAnimationEnd={() => autoPlay && select(active + 1)}\n                  />\n                </span>\n              )}\n            </button>\n          );\n        })}\n      </div>\n\n      <div\n        role=\"tabpanel\"\n        id={`${id}-p`}\n        aria-labelledby={`${id}-t${active}`}\n        className={cn(\"relative aspect-[4/3] w-full overflow-hidden rounded-2xl border border-border bg-muted shadow-2xl\", reverse && \"md:order-1\")}\n      >\n        {features.map((f, i) => (\n          <div\n            key={f.title}\n            aria-hidden={i !== active}\n            inert={i !== active}\n            className={cn(\n              \"fs-visual absolute inset-0 transition-[opacity,scale,filter] duration-700 ease-[cubic-bezier(.2,.8,.2,1)]\",\n              i === active ? \"scale-100 opacity-100 blur-0\" : \"pointer-events-none scale-[1.04] opacity-0 blur-sm\",\n            )}\n          >\n            {f.visual}\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n}\n"}]}