{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "hero-carousel",
  "type": "registry:block",
  "title": "Manual carousel hero",
  "description": "Split slides that change only on arrows, numbered tabs, Arrow keys or swipe. No autoplay, with carousel ARIA and a polite slide announcement.",
  "author": "StoreKit <hello@storekit.dev>",
  "categories": [
    "hero",
    "any"
  ],
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "https://storekit-eight.vercel.app/r/cn.json",
    "https://storekit-eight.vercel.app/r/sk-analytics.json"
  ],
  "files": [
    {
      "path": "hero/hero-carousel.tsx",
      "content": "\"use client\"\n\nimport { useId, useState } from \"react\"\nimport { AnimatePresence, motion, useReducedMotion, type PanInfo } from \"motion/react\"\nimport { ChevronLeft, ChevronRight } from \"lucide-react\"\nimport { cn } from \"@/components/storekit/lib/cn\"\nimport { useSkBlock } from \"@/components/storekit/lib/sk-analytics\"\n\n/* Manual hero carousel. There is no autoplay, on purpose: rotating heroes\n   have weak evidence behind them, later slides are rarely seen, and motion\n   the shopper did not ask for is an accessibility cost. Slides change only\n   from the arrows, the numbered tabs, Arrow keys while the carousel has\n   focus, or a horizontal swipe. Each slide is a split of copy and image that\n   stacks on mobile. The region is announced as a carousel and the live\n   region reports \"Slide 2 of 3\" only after a user action. */\n\nconst EASE = [0.23, 1, 0.32, 1] as const\n\nexport type CarouselSlide = {\n  image: string\n  alt: string\n  eyebrow?: string\n  title: string\n  body?: string\n  cta: { label: string; href: string }\n  focus?: string\n}\n\nconst SAMPLE: CarouselSlide[] = [\n  { image: \"/placeholder.webp\", alt: \"\", eyebrow: \"New\", title: \"Your morning, handled in one scoop.\", body: \"Greens, adaptogens and probiotics that taste good.\", cta: { label: \"Shop Daily Greens\", href: \"#\" } },\n  { image: \"/placeholder.webp\", alt: \"\", eyebrow: \"Bundle and save\", title: \"Build a routine, save 20%.\", body: \"Pick any three. Change or skip anytime.\", cta: { label: \"Build a bundle\", href: \"#\" } },\n  { image: \"/placeholder.webp\", alt: \"\", eyebrow: \"Ingredients\", title: \"Nothing hidden. Every batch tested.\", body: \"Read the lab report for the tub in your hand.\", cta: { label: \"See lab results\", href: \"#\" } },\n]\n\nconst PRIMARY =\n  \"sk-btn inline-flex min-h-12 items-center justify-center py-2.5 text-center leading-tight rounded-sk-btn bg-sk-accent px-7 text-[15px] font-medium text-sk-accent-ink outline-none transition-[filter,transform] duration-200 ease-sk-out hover:brightness-[1.06] focus-visible:ring-2 focus-visible:ring-sk-accent focus-visible:ring-offset-2 focus-visible:ring-offset-sk-paper active:scale-[0.97]\"\nconst ARROW =\n  \"grid h-11 w-11 place-items-center rounded-full border border-sk-line bg-sk-paper text-sk-ink outline-none transition-[background-color,transform] duration-200 ease-sk-out hover:bg-sk-surface focus-visible:ring-2 focus-visible:ring-sk-accent active:scale-[0.94]\"\n\nexport function HeroCarousel({\n  slides = SAMPLE,\n  label = \"Featured campaigns\",\n  className,\n}: {\n  slides?: CarouselSlide[]\n  /** Accessible name for the carousel region. */\n  label?: string\n  className?: string\n}) {\n  const reduce = useReducedMotion()\n  const sk = useSkBlock(\"hero\", \"hero-carousel\")\n  const uid = useId()\n  const [[index, dir], set] = useState<[number, number]>([0, 0])\n  const [announce, setAnnounce] = useState(false)\n  const n = slides.length\n  const s = slides[index]\n\n  const go = (next: number, how: string) => {\n    const i = (next + n) % n\n    if (i === index) return\n    set([i, next > index ? 1 : -1])\n    setAnnounce(true)\n    sk.emit(\"interact\", `slide_${how}`, i)\n  }\n\n  const onDragEnd = (_: unknown, info: PanInfo) => {\n    if (info.offset.x < -60 || info.velocity.x < -400) go(index + 1, \"swipe\")\n    else if (info.offset.x > 60 || info.velocity.x > 400) go(index - 1, \"swipe\")\n  }\n\n  const shift = reduce ? 0 : 40\n\n  return (\n    <section\n      ref={sk.ref}\n      {...sk.attrs}\n      aria-roledescription=\"carousel\"\n      aria-label={label}\n      onKeyDown={(e) => {\n        if (e.target instanceof HTMLElement && e.target.closest(\"a\")) return\n        if (e.key === \"ArrowRight\") go(index + 1, \"key\")\n        else if (e.key === \"ArrowLeft\") go(index - 1, \"key\")\n      }}\n      className={cn(\"bg-sk-paper text-sk-ink\", className)}\n    >\n      <div className=\"mx-auto w-full max-w-6xl px-4 py-8 sm:px-8 sm:py-12\">\n        <div className=\"relative overflow-hidden rounded-sk-card border border-sk-line bg-sk-surface\">\n          <AnimatePresence initial={false} custom={dir} mode=\"popLayout\">\n            <motion.div\n              key={index}\n              id={`${uid}-slide-${index}`}\n              role=\"group\"\n              aria-roledescription=\"slide\"\n              aria-label={`${index + 1} of ${n}`}\n              custom={dir}\n              initial={{ opacity: 0, x: dir * shift }}\n              animate={{ opacity: 1, x: 0 }}\n              exit={{ opacity: 0, x: dir * -shift }}\n              transition={reduce ? { duration: 0.2 } : { type: \"spring\", stiffness: 260, damping: 32, mass: 0.9 }}\n              drag={n > 1 ? \"x\" : false}\n              dragConstraints={{ left: 0, right: 0 }}\n              dragElastic={0.12}\n              onDragEnd={onDragEnd}\n              className=\"grid touch-pan-y items-stretch lg:min-h-[520px] lg:grid-cols-[1fr_1.1fr]\"\n            >\n              <div className=\"relative order-1 aspect-[4/3] lg:order-2 lg:aspect-auto\">\n                <img src={s.image} alt={s.alt} draggable={false} className=\"absolute inset-0 h-full w-full select-none object-cover\" style={{ objectPosition: s.focus }} />\n              </div>\n              <div className=\"order-2 flex flex-col justify-center bg-sk-paper p-6 pb-20 sm:p-10 sm:pb-24 lg:order-1 lg:bg-sk-surface lg:p-12 lg:pb-24\">\n                {s.eyebrow ? <p className=\"font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-muted\">{s.eyebrow}</p> : null}\n                {index === 0 ? (\n                  <h1 className=\"mt-4 text-balance font-sk-display font-semibold leading-[1.08] tracking-[-0.02em]\" style={{ fontSize: \"clamp(2rem, 4vw, 3.25rem)\" }}>\n                    {s.title}\n                  </h1>\n                ) : (\n                  <h2 className=\"mt-4 text-balance font-sk-display font-semibold leading-[1.08] tracking-[-0.02em]\" style={{ fontSize: \"clamp(2rem, 4vw, 3.25rem)\" }}>\n                    {s.title}\n                  </h2>\n                )}\n                {s.body ? <p className=\"mt-4 max-w-md text-[15px] leading-[1.55] text-sk-muted\">{s.body}</p> : null}\n                <div className=\"mt-7\">\n                  <a href={s.cta.href} onClick={() => sk.emit(\"interact\", \"cta\", index)} className={PRIMARY}>\n                    {s.cta.label}\n                  </a>\n                </div>\n              </div>\n            </motion.div>\n          </AnimatePresence>\n\n          {n > 1 ? (\n            <div className=\"absolute bottom-4 left-6 right-6 flex items-center justify-between gap-4 sm:left-10 sm:right-10 lg:left-12 lg:right-auto lg:w-[calc(50%-6rem)]\">\n              <div role=\"tablist\" aria-label=\"Choose slide\" className=\"flex items-center gap-1\">\n                {slides.map((sl, i) => (\n                  <button\n                    key={sl.title + i}\n                    type=\"button\"\n                    role=\"tab\"\n                    aria-selected={i === index}\n                    aria-controls={`${uid}-slide-${i}`}\n                    aria-label={`Slide ${i + 1}: ${sl.title}`}\n                    onClick={() => go(i, \"tab\")}\n                    className=\"group grid h-11 min-w-8 place-items-center outline-none focus-visible:rounded-sk-chip focus-visible:ring-2 focus-visible:ring-sk-accent\"\n                  >\n                    <span className={cn(\"block h-1 rounded-full transition-all duration-300 ease-sk-out\", i === index ? \"w-8 bg-sk-ink\" : \"w-4 bg-sk-ink/25 group-hover:bg-sk-ink/45\")} />\n                  </button>\n                ))}\n              </div>\n              <div className=\"flex gap-2\">\n                <button type=\"button\" aria-label=\"Previous slide\" onClick={() => go(index - 1, \"prev\")} className={ARROW}>\n                  <ChevronLeft size={18} aria-hidden />\n                </button>\n                <button type=\"button\" aria-label=\"Next slide\" onClick={() => go(index + 1, \"next\")} className={ARROW}>\n                  <ChevronRight size={18} aria-hidden />\n                </button>\n              </div>\n            </div>\n          ) : null}\n        </div>\n        <p className=\"sr-only\" aria-live=\"polite\" aria-atomic=\"true\">\n          {announce ? `Slide ${index + 1} of ${n}: ${s.title}` : \"\"}\n        </p>\n      </div>\n    </section>\n  )\n}\n",
      "type": "registry:block",
      "target": "components/storekit/hero/hero-carousel.tsx"
    }
  ],
  "docs": "Evidence for rotating heroes is weak: later slides are rarely seen. Prefer hero-multi-panel or one strong hero, and use this only for genuinely parallel campaigns. Autoplay is intentionally not supported.",
  "meta": {
    "section": "hero",
    "variant": "carousel",
    "niche": [
      "any"
    ],
    "commerce": [
      "cta"
    ],
    "previewHeight": 600
  }
}