{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "scroll-stack",
  "title": "Scroll Stack",
  "author": "AlgorizeTech",
  "description": "Ask the user to pick one of 6 scroll stack variants: https://avaraui.com/view/scroll-stack. This is the Scroll stack. Skip the question if the user already named one. Cards that stack and scale as the page scrolls, revealing each in turn. Use it for step-by-step feature walkthroughs.",
  "dependencies": [],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/custom/blocks/ScrollStackBlock.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport React, { useEffect, useRef } from \"react\";\n\n// ==========================================\n// TYPES & INTERFACES\n// ==========================================\nexport interface StackItem {\n  id: string;\n  title: string;\n  description: string;\n  content?: React.ReactNode;\n  /** Custom background color hex (e.g. \"#1e3a8a\"), CSS color, or Tailwind classes (e.g. \"bg-blue-900\") */\n  color?: string;\n  /** Optional alias for color */\n  bgColor?: string;\n  icon?: React.ElementType;\n}\n\nexport interface ScrollStackProps extends React.HTMLAttributes<HTMLDivElement> {\n  items: StackItem[];\n  depthScale?: number; // How much smaller background cards get (default: 0.05)\n  cardGap?: number; // Pixel distance between stacked sticky cards (default: 24)\n  scroller?: string | Element | null; // Scroll viewport driving the effect\n}\n\n// Helper to determine if a color string is a direct CSS color (hex, rgb, hsl, var)\nconst isCssColor = (c?: string): boolean => {\n  if (!c) return false;\n  return (\n    c.startsWith(\"#\") ||\n    c.startsWith(\"rgb\") ||\n    c.startsWith(\"hsl\") ||\n    c.startsWith(\"var(\")\n  );\n};\n\n// ==========================================\n// MAIN CONTAINER (SCROLL-DRIVEN)\n// ==========================================\nexport const ScrollStack = ({\n  items,\n  depthScale = 0.05,\n  cardGap = 24,\n  scroller,\n  className,\n  ...props\n}: ScrollStackProps) => {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const cardRefs = useRef<(HTMLDivElement | null)[]>([]);\n\n  useEffect(() => {\n    const container = containerRef.current;\n    if (!container) return;\n\n    // Resolve everything against the document the component actually lives in,\n    // so the effect also works when rendered inside an iframe.\n    const doc = container.ownerDocument;\n    const win = doc.defaultView ?? window;\n    let scrollerEl =\n      typeof scroller === \"string\"\n        ? doc.querySelector(scroller)\n        : (scroller ?? null);\n    // No explicit scroller (or a ref that was still null when passed):\n    // fall back to the nearest scrollable ancestor.\n    if (!scrollerEl) {\n      let parent = container.parentElement;\n      while (parent && parent !== doc.documentElement) {\n        const style = win.getComputedStyle(parent);\n        if (\n          /(auto|scroll|overlay)/.test(style.overflowY) &&\n          parent.scrollHeight > parent.clientHeight\n        ) {\n          scrollerEl = parent;\n          break;\n        }\n        parent = parent.parentElement;\n      }\n    }\n\n    // Scrub each card as the NEXT card scrolls from 85% to 20% of the viewport\n    const update = () => {\n      const viewportTop = scrollerEl\n        ? scrollerEl.getBoundingClientRect().top\n        : 0;\n      const viewportHeight = scrollerEl\n        ? scrollerEl.clientHeight\n        : win.innerHeight;\n      const start = viewportHeight * 0.85;\n      const end = viewportHeight * 0.2;\n\n      cardRefs.current.forEach((card, index) => {\n        if (!card || index === items.length - 1) return; // Last card doesn't shrink\n        const nextCard = cardRefs.current[index + 1];\n        if (!nextCard) return;\n\n        const nextTop = nextCard.getBoundingClientRect().top - viewportTop;\n        const progress = Math.min(\n          1,\n          Math.max(0, (start - nextTop) / (start - end)),\n        );\n\n        const targetScale = 1 - (items.length - 1 - index) * depthScale;\n        card.style.transform = `scale(${1 + (targetScale - 1) * progress})`;\n        card.style.opacity = String(1 - 0.35 * progress);\n      });\n    };\n\n    update();\n    const scrollTarget: EventTarget = scrollerEl ?? win;\n    scrollTarget.addEventListener(\"scroll\", update, { passive: true });\n    win.addEventListener(\"resize\", update);\n\n    return () => {\n      scrollTarget.removeEventListener(\"scroll\", update);\n      win.removeEventListener(\"resize\", update);\n      cardRefs.current.forEach((card) => {\n        if (card) {\n          card.style.transform = \"\";\n          card.style.opacity = \"\";\n        }\n      });\n    };\n  }, [items.length, depthScale, scroller]);\n\n  return (\n    <div\n      ref={containerRef}\n      className={cn(\"relative w-full\", className)}\n      style={{ paddingBottom: \"15vh\" }}\n      {...props}\n    >\n      {items.map((item, index) => {\n        const customColor = item.color || item.bgColor;\n        const hasCssBg = Boolean(customColor && isCssColor(customColor));\n\n        return (\n          <div\n            key={item.id}\n            className=\"flex min-h-screen w-full items-center justify-center sticky\"\n            // Each card sticks a little lower than the one before, which is\n            // what makes the stack fan out rather than pile up exactly.\n            style={{ top: index * cardGap }}\n          >\n            <div\n              ref={(el) => {\n                cardRefs.current[index] = el;\n              }}\n              style={\n                hasCssBg && customColor\n                  ? { backgroundColor: customColor }\n                  : undefined\n              }\n              className={cn(\n                \"relative w-full max-w-4xl h-[60vh] sm:h-[65vh] rounded-2xl p-8 md:p-12 shadow-xl dark:shadow-2xl origin-top flex flex-col md:flex-row gap-8 overflow-hidden border transition-colors duration-300\",\n                hasCssBg\n                  ? \"border-white/10 text-white\"\n                  : customColor\n                    ? customColor\n                    : \"bg-card border-border text-foreground hover:border-[var(--theme-color,var(--primary))]\",\n              )}\n            >\n              {/* Subtle Glass Noise Overlay */}\n              <div className=\"absolute inset-0 bg-foreground/[0.02] pointer-events-none\" />\n\n              <div className=\"flex-1 flex flex-col justify-center relative z-10\">\n                {item.icon && (\n                  <div\n                    className={cn(\n                      \"w-14 h-14 rounded-2xl flex items-center justify-center mb-6 shadow-sm transition-colors\",\n                      customColor\n                        ? \"bg-white/15 border border-white/20 text-white\"\n                        : \"bg-[var(--theme-color,var(--primary))]/10 border border-[var(--theme-color,var(--primary))]/20 text-[var(--theme-color,var(--primary))]\"\n                    )}\n                  >\n                    <item.icon className=\"w-7 h-7\" />\n                  </div>\n                )}\n                <h2 className=\"text-3xl md:text-4xl lg:text-5xl font-extrabold tracking-tight mb-4 leading-tight\">\n                  {item.title}\n                </h2>\n                <p\n                  className={cn(\n                    \"text-base sm:text-lg font-medium leading-relaxed max-w-lg transition-colors\",\n                    customColor\n                      ? \"text-white/85\"\n                      : \"text-muted-foreground\",\n                  )}\n                >\n                  {item.description}\n                </p>\n              </div>\n\n              {/* Right-Side Content Payload */}\n              {item.content && (\n                <div\n                  className={cn(\n                    \"flex-1 relative z-10 rounded-2xl overflow-hidden hidden md:flex items-center justify-center transition-colors\",\n                    customColor\n                      ? \"bg-black/20 border border-white/10 shadow-lg\"\n                      : \"bg-muted/80 border border-border/80\"\n                  )}\n                >\n                  {item.content}\n                </div>\n              )}\n            </div>\n          </div>\n        );\n      })}\n    </div>\n  );\n};\n",
      "type": "registry:component",
      "target": "components/custom/blocks/ScrollStackBlock.tsx"
    }
  ],
  "docs": "Named exports: ScrollStack, StackItem. `items` take { id, title, description?, content?, color?, icon? }; `depthScale` and `cardGap` tune the stack. Pass `scroller` when the page scrolls inside a container rather than the window. No runtime dependencies.",
  "categories": [
    "scroll",
    "marketing"
  ],
  "type": "registry:component"
}
