{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "split-scroll-stack",
  "title": "Split 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 Split scroll stack. Skip the question if the user already named one. A two-column scroll stack with a pinned side panel and cards that advance alongside it. Use it for feature tours that pair narrative with visuals.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/custom/blocks/SplitScrollStackBlock.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { ArrowDownRight, CheckCircle2, Layers } from \"lucide-react\";\nimport React, { useEffect, useRef, useState } from \"react\";\n\n// ==========================================\n// CARD DECK MOTION (CSS transitions)\n// ==========================================\n/** GSAP's power3.out, sampled into a CSS `linear()` curve. */\nconst EASE_POWER3_OUT =\n  \"linear(0, .0963, .1855, .2679, .3439, .4138, .478, .5367, .5904, .6392, .6836, .7237, .7599, .7924, .8215, .8474, .8704, .8907, .9085, .924, .9375, .9491, .959, .9674, .9744, .9802, .985, .9888, .9919, .9943, .9961, .9974, .9984, .9991, .9995, .9998, .9999, 1, 1, 1, 1)\";\n\n/** The card also carries `transition-colors`; its transition is kept alongside the deck motion. */\nconst CARD_CLASS_TRANSITION = [\"color\", \"background-color\", \"border-color\", \"outline-color\", \"text-decoration-color\", \"fill\", \"stroke\", \"--tw-gradient-from\", \"--tw-gradient-via\", \"--tw-gradient-to\"];\n\n/**\n * Where a card sits in the deck — stacked behind the active card, at the\n * front, or waiting below — as a style that transitions there from wherever\n * the card is now.\n */\nfunction deckCardStyle(\n  idx: number,\n  active: number,\n  count: number,\n  cardOffset: number,\n  cardDepthScale: number,\n  upcomingY: number,\n): React.CSSProperties {\n  let y = 0;\n  let scale = 1;\n  let opacity = 1;\n  let zIndex = count + 10;\n  let duration = 0.6;\n\n  if (idx < active) {\n    // Stacked behind in the deck\n    const depth = active - idx;\n    y = -depth * cardOffset;\n    scale = 1 - depth * cardDepthScale;\n    opacity = Math.max(0.2, 1 - depth * 0.25);\n    zIndex = idx;\n  } else if (idx > active) {\n    // Upcoming, hidden below\n    y = upcomingY;\n    scale = 0.95;\n    opacity = 0;\n    zIndex = idx;\n    duration = 0.5;\n  }\n\n  const deck = [\"transform\", \"opacity\", \"z-index\"];\n  return {\n    transform: `translateY(${y}px) scale(${scale})`,\n    opacity,\n    zIndex,\n    transitionProperty: [...deck, ...CARD_CLASS_TRANSITION].join(\", \"),\n    transitionDuration: [\n      ...deck.map(() => `${duration}s`),\n      ...CARD_CLASS_TRANSITION.map(\n        () => \"var(--tw-duration, var(--default-transition-duration, 150ms))\",\n      ),\n    ].join(\", \"),\n    transitionTimingFunction: [\n      ...deck.map(() => EASE_POWER3_OUT),\n      ...CARD_CLASS_TRANSITION.map(\n        () =>\n          \"var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(0.4, 0, 0.2, 1)))\",\n      ),\n    ].join(\", \"),\n  };\n}\n\n// ==========================================\n// TYPES & INTERFACES\n// ==========================================\nexport interface ScrollStackItem {\n  id: string;\n  stepNumber: string; // e.g., \"01\", \"02\"\n  title: string;\n  description: string;\n  tags?: string[];\n  cardContent: React.ReactNode;\n  color?: string; // Optional accent surface classes\n}\n\nexport interface SplitScrollStackProps\n  extends React.HTMLAttributes<HTMLDivElement> {\n  items: ScrollStackItem[];\n  stickySide?: \"left\" | \"right\"; // Swaps sticky card deck vs scrolling text\n  cardDepthScale?: number; // Shrink rate of stacked background cards (default: 0.05)\n  cardOffset?: number; // Vertical pixel distance between stacked cards (default: 16)\n  color?: string; // Global active accent colour (default: the theme's primary)\n  scroller?: string | Element | null; // Optional direct element scroller target\n  scrollerRef?: React.RefObject<HTMLElement | null>; // CRITICAL: Ref object for React containers\n  onActiveIndexChange?: (index: number) => void;\n}\n\n// ==========================================\n// SPLIT-SCROLLSTACK ENGINE\n// ==========================================\nexport const SplitScrollStack = React.forwardRef<\n  HTMLDivElement,\n  SplitScrollStackProps\n>(\n  (\n    {\n      items,\n      stickySide = \"right\",\n      cardDepthScale = 0.05,\n      cardOffset = 16,\n      color = \"var(--primary)\",\n      scroller,\n      scrollerRef,\n      className,\n      onActiveIndexChange,\n      ...props\n    },\n    ref,\n  ) => {\n    const [activeIndex, setActiveIndex] = useState(0);\n    const containerRef = useRef<HTMLDivElement>(null);\n    const sectionRefs = useRef<(HTMLDivElement | null)[]>([]);\n\n    // Keep callback fresh without triggering useEffect re-runs\n    const onActiveIndexChangeRef = useRef(onActiveIndexChange);\n    useEffect(() => {\n      onActiveIndexChangeRef.current = onActiveIndexChange;\n    }, [onActiveIndexChange]);\n\n    React.useImperativeHandle(\n      ref,\n      () => containerRef.current as HTMLDivElement,\n    );\n\n    // Scroll Tracking — a section is active while it spans the band between\n    // 40% and 60% of the scroller's viewport (its top above the 40% line,\n    // its bottom below the 60% line).\n    useEffect(() => {\n      if (!containerRef.current) return;\n\n      // CRITICAL FIX: Resolve scroller target AFTER mount guaranteed by useEffect\n      const resolved = scrollerRef?.current || scroller || window;\n      const target: HTMLElement | Window | null =\n        typeof resolved === \"string\"\n          ? document.querySelector<HTMLElement>(resolved)\n          : (resolved as HTMLElement | Window);\n      if (!target) return;\n\n      const wasActive: boolean[] = [];\n\n      const check = () => {\n        let viewportTop = 0;\n        let viewportHeight = window.innerHeight;\n        if (target !== window) {\n          const el = target as HTMLElement;\n          viewportTop = el.getBoundingClientRect().top + el.clientTop;\n          viewportHeight = el.clientHeight;\n        }\n\n        sectionRefs.current.forEach((section, idx) => {\n          if (!section) return;\n          const rect = section.getBoundingClientRect();\n          const isActive =\n            rect.top - viewportTop < viewportHeight * 0.4 &&\n            rect.bottom - viewportTop > viewportHeight * 0.6;\n          // Fire only on becoming active, which prevents skipping/dead zones\n          // on fast scrolling — the last active section stays active.\n          if (isActive && !wasActive[idx]) {\n            setActiveIndex(idx);\n            onActiveIndexChangeRef.current?.(idx);\n          }\n          wasActive[idx] = isActive;\n        });\n      };\n\n      let frame = 0;\n      const onScroll = () => {\n        if (frame) return;\n        frame = requestAnimationFrame(() => {\n          frame = 0;\n          check();\n        });\n      };\n\n      check();\n      target.addEventListener(\"scroll\", onScroll, { passive: true });\n      window.addEventListener(\"resize\", onScroll);\n\n      // Re-check after layout settles\n      const timer = setTimeout(check, 150);\n\n      return () => {\n        clearTimeout(timer);\n        cancelAnimationFrame(frame);\n        target.removeEventListener(\"scroll\", onScroll);\n        window.removeEventListener(\"resize\", onScroll);\n      };\n    }, [scroller, scrollerRef, items.length]);\n\n    return (\n      <div\n        ref={containerRef}\n        className={cn(\n          \"grid grid-cols-1 md:grid-cols-12 gap-8 lg:gap-16 w-full relative\",\n          className,\n        )}\n        {...props}\n      >\n        {/* ========================================== */}\n        {/* COLUMN A: SCROLLING NARRATIVE SECTIONS     */}\n        {/* ========================================== */}\n        <div\n          className={cn(\n            \"col-span-1 md:col-span-6 lg:col-span-7 flex flex-col gap-12 sm:gap-24 relative z-10 py-6 sm:py-12\",\n            stickySide === \"left\" ? \"md:order-2\" : \"md:order-1\",\n          )}\n        >\n          {items.map((item, index) => {\n            const isActive = activeIndex === index;\n            const isPassed = activeIndex > index;\n\n            return (\n              <div\n                key={item.id}\n                ref={(el) => {\n                  sectionRefs.current[index] = el;\n                }}\n                className={cn(\n                  \"group relative rounded-3xl p-6 sm:p-10 border transition-all duration-500 min-h-[50vh] sm:min-h-[60vh] flex flex-col justify-center\",\n                  isActive\n                    ? \"bg-card shadow-2xl border-transparent scale-[1.01]\"\n                    : \"bg-muted/50 border-border/80 opacity-60 hover:opacity-90\",\n                )}\n                style={{\n                  borderColor: isActive ? color : undefined,\n                }}\n              >\n                {/* Active Side Glowing Bar */}\n                {isActive && (\n                  <div\n                    className=\"absolute top-6 bottom-6 left-0 w-1.5 rounded-r-full transition-colors duration-300\"\n                    style={{ backgroundColor: color }}\n                  />\n                )}\n\n                <div className=\"flex items-start justify-between gap-4\">\n                  <div className=\"flex items-center gap-3.5\">\n                    {/* Step Badge */}\n                    <span\n                      className={cn(\n                        \"w-11 h-11 rounded-xl flex items-center justify-center font-mono font-semibold text-sm border transition-all duration-300 shadow-sm\",\n                        isActive\n                          ? \"text-primary-foreground border-transparent scale-110\"\n                          : isPassed\n                            ? \"bg-muted text-foreground border-transparent\"\n                            : \"bg-muted text-muted-foreground border-border\",\n                      )}\n                      style={{\n                        backgroundColor: isActive ? color : undefined,\n                      }}\n                    >\n                      {isPassed ? (\n                        <CheckCircle2 className=\"w-5 h-5\" />\n                      ) : (\n                        item.stepNumber\n                      )}\n                    </span>\n\n                    <h3\n                      className={cn(\n                        \"text-2xl sm:text-3xl font-bold tracking-tight transition-colors duration-300\",\n                        isActive\n                          ? \"text-foreground\"\n                          : \"text-muted-foreground\",\n                      )}\n                    >\n                      {item.title}\n                    </h3>\n                  </div>\n\n                  {/* Scroll Indicator Icon */}\n                  <div\n                    className={cn(\n                      \"w-10 h-10 rounded-full flex items-center justify-center transition-all duration-300 flex-shrink-0\",\n                      isActive\n                        ? \"opacity-100 scale-100 shadow-sm\"\n                        : \"opacity-0 scale-75\",\n                    )}\n                    style={{\n                      backgroundColor: isActive ? (color.startsWith('#') ? `${color}15` : `color-mix(in srgb, ${color} 15%, transparent)`) : \"transparent\",\n                      color: isActive ? color : \"currentColor\",\n                    }}\n                  >\n                    <ArrowDownRight className=\"w-5 h-5 animate-bounce\" />\n                  </div>\n                </div>\n\n                {/* Description */}\n                <p\n                  className={cn(\n                    \"mt-6 text-base sm:text-lg leading-relaxed transition-colors duration-300 pl-0 sm:pl-[58px]\",\n                    isActive\n                      ? \"text-muted-foreground\"\n                      : \"text-muted-foreground\",\n                  )}\n                >\n                  {item.description}\n                </p>\n\n                {/* Feature Tags Array */}\n                {item.tags && item.tags.length > 0 && (\n                  <div className=\"flex flex-wrap gap-2.5 mt-8 pl-0 sm:pl-[58px]\">\n                    {item.tags.map((tag, tIdx) => (\n                      <span\n                        key={tIdx}\n                        className={cn(\n                          \"px-3.5 py-1.5 rounded-xl text-xs font-semibold uppercase tracking-wider border transition-all duration-300\",\n                          isActive\n                            ? \"border-transparent shadow-xs font-extrabold\"\n                            : \"bg-transparent border-border text-muted-foreground/70\",\n                        )}\n                        style={{\n                          backgroundColor: isActive ? (color.startsWith('#') ? `${color}15` : `color-mix(in srgb, ${color} 15%, transparent)`) : undefined,\n                          color: isActive ? color : undefined,\n                        }}\n                      >\n                        {tag}\n                      </span>\n                    ))}\n                  </div>\n                )}\n              </div>\n            );\n          })}\n        </div>\n\n        {/* ========================================== */}\n        {/* COLUMN B: STICKY 3D VISUAL DECK            */}\n        {/* ========================================== */}\n        <div\n          className={cn(\n            \"hidden md:block md:col-span-6 lg:col-span-5 w-full relative z-20\", // Added hidden md:block!\n            stickySide === \"left\" ? \"md:order-1\" : \"md:order-2\",\n          )}\n        >\n          {/* Decoupled sticky box inside stretching column track */}\n          <div className=\"sticky top-20 sm:top-28 w-full h-[450px] sm:h-[520px] flex items-center justify-center perspective-[1200px] py-4\">\n            <div className=\"relative w-full h-full max-w-lg\">\n              {items.map((item, index) => {\n                return (\n                  <div\n                    key={item.id}\n                    className={cn(\n                      \"absolute inset-0 w-full h-full rounded-2xl p-6 sm:p-8 border overflow-hidden shadow-xl dark:shadow-2xl flex flex-col justify-between origin-top transition-colors\",\n                      item.color ||\n                        \"bg-card text-foreground border-border\",\n                    )}\n                    style={deckCardStyle(\n                      index,\n                      activeIndex,\n                      items.length,\n                      cardOffset,\n                      cardDepthScale,\n                      140,\n                    )}\n                  >\n                    {/* Glassmorphic Top Highlight */}\n                    <div className=\"absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-white/30 to-transparent pointer-events-none\" />\n\n                    {/* Card Header */}\n                    <div className=\"flex items-center justify-between relative z-10 mb-4\">\n                      <span className=\"px-3.5 py-1 rounded-full bg-muted text-[11px] font-mono font-semibold tracking-widest uppercase border border-border flex items-center gap-2 text-foreground\">\n                        <Layers className=\"w-3.5 h-3.5\" /> DECK //{\" \"}\n                        {item.stepNumber}\n                      </span>\n                      <div\n                        className=\"w-2.5 h-2.5 rounded-full animate-ping\"\n                        style={{ backgroundColor: color }}\n                      />\n                    </div>\n\n                    {/* Visual Payload */}\n                    <div className=\"relative z-10 flex-1 flex flex-col justify-center my-2 overflow-hidden\">\n                      {item.cardContent}\n                    </div>\n\n                    {/* Card Footer */}\n                    <div className=\"relative z-10 pt-4 border-t border-border flex items-center justify-between text-xs text-muted-foreground font-medium\">\n                      <span className=\"truncate pr-2 font-semibold text-foreground\">\n                        {item.title}\n                      </span>\n                      <span className=\"font-mono text-[10px] uppercase tracking-wider text-muted-foreground/70 flex-shrink-0\">\n                        SCROLL DRIVEN\n                      </span>\n                    </div>\n                  </div>\n                );\n              })}\n            </div>\n          </div>\n        </div>\n      </div>\n    );\n  },\n);\nSplitScrollStack.displayName = \"SplitScrollStack\";\n",
      "type": "registry:component",
      "target": "components/custom/blocks/SplitScrollStackBlock.tsx"
    }
  ],
  "docs": "Named export: SplitScrollStack. `stickySide` pins left or right; `cardDepthScale` and `cardOffset` tune the stack. Driven by GSAP ScrollTrigger, so it needs real page height — it will look inert in a short container. Same family as `split-stepper`.",
  "categories": [
    "scroll",
    "marketing"
  ],
  "type": "registry:component"
}
