{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "process-card-reveal",
  "type": "registry:block",
  "title": "Process Card Reveal",
  "description": "Numbered steps on a timeline whose connector fills before the next card slides and blurs into place — image one way, copy the other. Hover pauses the loop; clicking a step jumps there.",
  "dependencies": [
    "gsap",
    "@gsap/react"
  ],
  "registryDependencies": [],
  "cssVars": {
    "theme": {}
  },
  "files": [
    {
      "path": "components/tweenui/process-card-reveal.tsx",
      "type": "registry:component",
      "target": "components/tweenui/process-card-reveal.tsx",
      "content": "'use client';\n\nimport { useRef, useState, type ComponentPropsWithoutRef } from 'react';\nimport { useGSAP } from '@gsap/react';\nimport gsap from 'gsap';\nimport { cn } from '@/lib/utils';\n\ngsap.registerPlugin(useGSAP);\n\nconst prefersReducedMotion = () =>\n  typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\nconst PROCESS_ANIMATION = {\n  STEP_WAIT: 1.2,\n  LINE_DURATION: 1,\n  OPEN_DUR: 1,\n  CLOSE_DUR: 0.8,\n  EASE: 'cubic-bezier(0.22, 1, 0.36, 1)',\n  TEXT_STAGGER: 0.3,\n  BLUR_IN: 'blur(3px)',\n  BLUR_OUT: 'blur(0px)',\n} as const;\n\nconst PROCESS_REVEAL_DUR = Math.max(\n  PROCESS_ANIMATION.CLOSE_DUR,\n  PROCESS_ANIMATION.OPEN_DUR + PROCESS_ANIMATION.TEXT_STAGGER\n);\n\nconst overlayFigureStyle = { boxShadow: '0 1px 0 0 rgba(255, 255, 255, 0.4) inset' };\n\nexport interface ProcessStepDetail {\n  title: string;\n  description: string;\n}\n\nexport interface ProcessStep {\n  /** Stable id used as the React key. */\n  id: string;\n  /** Step heading shown in the card copy. */\n  title: string;\n  /** Supporting line under the title. */\n  description: string;\n  /** Photo URL for the card. */\n  image: string;\n  /** Alt text for the photo. */\n  imageAlt: string;\n  /** Optional overlay image sitting on the photo. */\n  overlay?: string;\n  /** Alt text for the overlay image. */\n  overlayAlt?: string;\n  /** Detail rows shown beside the photo on desktop. */\n  details: ProcessStepDetail[];\n}\n\nexport interface ProcessCardRevealProps extends Omit<ComponentPropsWithoutRef<'section'>, 'title'> {\n  /** Process steps shown in the stacked cards. Defaults to a 4-step sample. */\n  steps?: ProcessStep[];\n}\n\nconst DEFAULT_STEPS: ProcessStep[] = [\n  {\n    id: 'connect',\n    title: 'Connect apps',\n    description:\n      'Link your favorite tools in seconds and start building workflows without any setup complexity.',\n    image: 'https://images.unsplash.com/photo-1551434678-e076c223a692?w=960&h=720&fit=crop',\n    imageAlt: 'Team connecting apps on a laptop',\n    details: [\n      {\n        title: 'One-Click Integrations',\n        description: 'Connect popular apps instantly — no technical setup required.',\n      },\n      {\n        title: 'Secure Connections',\n        description: 'Your data stays protected with enterprise-grade security.',\n      },\n    ],\n  },\n  {\n    id: 'build',\n    title: 'Build flows',\n    description:\n      'Combine your connected apps into clear, repeatable automation in a few simple steps.',\n    image: 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=960&h=720&fit=crop',\n    imageAlt: 'Building an automation flow on a dashboard',\n    details: [\n      {\n        title: 'Drag And Drop Builder',\n        description: 'Arrange triggers and actions visually with zero coding effort.',\n      },\n      {\n        title: 'Reusable Templates',\n        description: 'Start from prebuilt flows and customize them in minutes.',\n      },\n    ],\n  },\n  {\n    id: 'test',\n    title: 'Test and optimize',\n    description:\n      'Validate every step, monitor results, and improve your workflow performance over time.',\n    image: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=960&h=720&fit=crop',\n    imageAlt: 'Analytics charts used to test and optimize a flow',\n    details: [\n      {\n        title: 'Live Status Tracking',\n        description: 'See exactly how each run performs and catch issues instantly.',\n      },\n      {\n        title: 'Smart Insights',\n        description: 'Use actionable analytics to refine every stage of your flow.',\n      },\n    ],\n  },\n  {\n    id: 'scale',\n    title: 'Scale with confidence',\n    description:\n      'Launch automation across your team while keeping speed, reliability, and security under control.',\n    image: 'https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=960&h=720&fit=crop',\n    imageAlt: 'Team collaborating as they scale automation',\n    details: [\n      {\n        title: 'Team Collaboration',\n        description: 'Share workflows and permissions with clear ownership.',\n      },\n      {\n        title: 'Enterprise Ready',\n        description: 'Keep every process compliant and secure as you grow.',\n      },\n    ],\n  },\n];\n\ntype StepState = 'active' | 'completed' | 'inactive';\n\ntype TimelineBundle = {\n  lines: HTMLElement[];\n};\n\nfunction isLineHorizontal(el: HTMLElement) {\n  const axis = el.dataset.lineAxis;\n  if (axis === 'horizontal') return true;\n  if (axis === 'vertical') return false;\n  return el.offsetWidth > el.offsetHeight;\n}\n\nfunction collect(items: Array<HTMLElement | null>) {\n  return items.filter((item): item is HTMLElement => item !== null);\n}\n\nfunction initTimelineLines(timelines: TimelineBundle[]) {\n  timelines.forEach(({ lines }) => {\n    lines.forEach((line) => {\n      if (isLineHorizontal(line)) gsap.set(line, { transformOrigin: 'left center', scaleX: 0 });\n      else gsap.set(line, { transformOrigin: 'top center', scaleY: 0 });\n    });\n  });\n}\n\nfunction setTimelineLineProgress(timelines: TimelineBundle[], index: number) {\n  timelines.forEach(({ lines }) => {\n    lines.forEach((line, i) => {\n      if (isLineHorizontal(line)) gsap.set(line, { scaleX: i < index ? 1 : 0 });\n      else gsap.set(line, { scaleY: i < index ? 1 : 0 });\n    });\n  });\n}\n\nfunction animateTimelineLine(\n  timelines: TimelineBundle[],\n  lineIndex: number,\n  timeline: gsap.core.Timeline\n) {\n  timelines.forEach(({ lines }) => {\n    const line = lines[lineIndex];\n    if (!line) return;\n\n    if (isLineHorizontal(line))\n      timeline.to(\n        line,\n        { scaleX: 1, duration: PROCESS_ANIMATION.LINE_DURATION, ease: 'sine.inOut' },\n        0\n      );\n    else\n      timeline.to(\n        line,\n        { scaleY: 1, duration: PROCESS_ANIMATION.LINE_DURATION, ease: 'sine.inOut' },\n        0\n      );\n  });\n}\n\nfunction snapToCard({\n  nextIndex,\n  cards,\n  images,\n  contents,\n}: {\n  nextIndex: number;\n  cards: HTMLElement[];\n  images: HTMLElement[];\n  contents: HTMLElement[];\n}) {\n  cards.forEach((card, i) => {\n    gsap.set(card, {\n      pointerEvents: i === nextIndex ? 'auto' : 'none',\n      zIndex: i === nextIndex ? 2 : 0,\n      overflow: 'hidden',\n    });\n  });\n  images.forEach((image, i) => {\n    gsap.set(image, {\n      yPercent: 0,\n      opacity: i === nextIndex ? 1 : 0,\n      filter: PROCESS_ANIMATION.BLUR_OUT,\n    });\n  });\n  contents.forEach((content, i) => {\n    gsap.set(content, {\n      yPercent: 0,\n      opacity: i === nextIndex ? 1 : 0,\n      filter: PROCESS_ANIMATION.BLUR_OUT,\n    });\n  });\n}\n\nfunction playCardSlide({\n  current,\n  next,\n  currentImage,\n  currentText,\n  nextImage,\n  nextText,\n  reverse,\n  onComplete,\n}: {\n  current: HTMLElement;\n  next: HTMLElement;\n  currentImage?: HTMLElement;\n  currentText?: HTMLElement;\n  nextImage?: HTMLElement;\n  nextText?: HTMLElement;\n  reverse: boolean;\n  onComplete: () => void;\n}) {\n  const { CLOSE_DUR, OPEN_DUR, EASE, TEXT_STAGGER, BLUR_IN, BLUR_OUT } = PROCESS_ANIMATION;\n  const tl = gsap.timeline({ onComplete });\n\n  tl.set(next, { pointerEvents: 'auto', zIndex: 2, backgroundColor: 'transparent' }, 0);\n  tl.set(current, { zIndex: 3, backgroundColor: 'transparent' }, 0);\n\n  if (nextImage)\n    tl.set(nextImage, { yPercent: reverse ? -100 : 100, opacity: 0, filter: BLUR_IN }, 0);\n  if (nextText)\n    tl.set(nextText, { yPercent: reverse ? 100 : -100, opacity: 0, filter: BLUR_IN }, 0);\n\n  if (currentImage)\n    tl.to(\n      currentImage,\n      {\n        yPercent: reverse ? 100 : -100,\n        opacity: 0,\n        filter: BLUR_IN,\n        duration: CLOSE_DUR,\n        force3D: true,\n        ease: EASE,\n      },\n      0\n    );\n  if (nextImage)\n    tl.to(\n      nextImage,\n      { yPercent: 0, opacity: 1, filter: BLUR_OUT, duration: OPEN_DUR, force3D: true, ease: EASE },\n      0\n    );\n  if (currentText)\n    tl.to(\n      currentText,\n      {\n        yPercent: reverse ? -100 : 100,\n        opacity: 0,\n        filter: BLUR_IN,\n        duration: CLOSE_DUR,\n        force3D: true,\n        ease: EASE,\n      },\n      0\n    );\n  if (nextText)\n    tl.to(\n      nextText,\n      { yPercent: 0, opacity: 1, filter: BLUR_OUT, duration: OPEN_DUR, force3D: true, ease: EASE },\n      TEXT_STAGGER\n    );\n\n  tl.set(current, { pointerEvents: 'none', zIndex: 0, backgroundColor: '' }, PROCESS_REVEAL_DUR);\n  tl.set(next, { backgroundColor: '' }, PROCESS_REVEAL_DUR);\n  if (currentImage)\n    tl.set(currentImage, { yPercent: 0, opacity: 0, filter: BLUR_OUT }, PROCESS_REVEAL_DUR);\n  if (currentText)\n    tl.set(currentText, { yPercent: 0, opacity: 0, filter: BLUR_OUT }, PROCESS_REVEAL_DUR);\n\n  return tl;\n}\n\nexport default function ProcessCardReveal({\n  steps = DEFAULT_STEPS,\n  className,\n  ...props\n}: ProcessCardRevealProps) {\n  const cardCount = steps.length;\n  const lineCount = Math.max(cardCount - 1, 0);\n\n  const rootRef = useRef<HTMLElement>(null);\n  const cardsRef = useRef<Array<HTMLElement | null>>([]);\n  const imagesRef = useRef<Array<HTMLElement | null>>([]);\n  const contentsRef = useRef<Array<HTMLElement | null>>([]);\n  const horizontalLinesRef = useRef<Array<HTMLElement | null>>([]);\n  const verticalLinesRef = useRef<Array<HTMLElement | null>>([]);\n\n  const [activeIndex, setActiveIndex] = useState(0);\n  const activeIndexRef = useRef(0);\n  const busyRef = useRef(false);\n  const pausedRef = useRef(false);\n  const lineTlRef = useRef<gsap.core.Timeline | undefined>(undefined);\n  const slideTlRef = useRef<gsap.core.Timeline | undefined>(undefined);\n  const loopDelayRef = useRef<gsap.core.Tween | undefined>(undefined);\n\n  const goToStepRef = useRef<(index: number) => void>(() => {});\n  const pauseRef = useRef<() => void>(() => {});\n  const resumeRef = useRef<() => void>(() => {});\n\n  const getStepState = (index: number): StepState => {\n    if (index === activeIndex) return 'active';\n    if (index < activeIndex) return 'completed';\n    return 'inactive';\n  };\n\n  useGSAP(\n    (_context, contextSafe) => {\n      if (!contextSafe) return;\n\n      const cards = collect(cardsRef.current);\n      const images = collect(imagesRef.current);\n      const contents = collect(contentsRef.current);\n      const timelines: TimelineBundle[] = [\n        { lines: collect(horizontalLinesRef.current) },\n        { lines: collect(verticalLinesRef.current) },\n      ];\n\n      if (\n        cards.length !== cardCount ||\n        images.length !== cardCount ||\n        contents.length !== cardCount\n      )\n        return;\n\n      const reduced = prefersReducedMotion();\n\n      cards.forEach((card, index) => {\n        gsap.set(card, {\n          pointerEvents: index === 0 ? 'auto' : 'none',\n          zIndex: index === 0 ? 2 : 0,\n          overflow: 'hidden',\n        });\n      });\n      images.forEach((image, index) => {\n        gsap.set(image, {\n          yPercent: 0,\n          opacity: index === 0 ? 1 : 0,\n          filter: PROCESS_ANIMATION.BLUR_OUT,\n          willChange: 'transform, opacity, filter',\n        });\n      });\n      contents.forEach((content, index) => {\n        gsap.set(content, {\n          yPercent: 0,\n          opacity: index === 0 ? 1 : 0,\n          filter: PROCESS_ANIMATION.BLUR_OUT,\n          willChange: 'transform, opacity, filter',\n        });\n      });\n      initTimelineLines(timelines);\n      setTimelineLineProgress(timelines, 0);\n      activeIndexRef.current = 0;\n      setActiveIndex(0);\n      busyRef.current = false;\n      pausedRef.current = false;\n\n      const slideTo = contextSafe((nextIndex: number, reverse = false) => {\n        if (busyRef.current || nextIndex === activeIndexRef.current) return;\n\n        const current = cards[activeIndexRef.current];\n        const next = cards[nextIndex];\n        if (!current || !next) return;\n\n        const currentImage = images[activeIndexRef.current];\n        const currentText = contents[activeIndexRef.current];\n        const nextImage = images[nextIndex];\n        const nextText = contents[nextIndex];\n\n        activeIndexRef.current = nextIndex;\n        setActiveIndex(nextIndex);\n        setTimelineLineProgress(timelines, nextIndex);\n\n        if (reduced) {\n          snapToCard({ nextIndex, cards, images, contents });\n          return;\n        }\n\n        busyRef.current = true;\n        slideTlRef.current?.kill();\n        slideTlRef.current = playCardSlide({\n          current,\n          next,\n          currentImage,\n          currentText,\n          nextImage,\n          nextText,\n          reverse,\n          onComplete: () => {\n            busyRef.current = false;\n          },\n        });\n      });\n\n      const scheduleLoop = contextSafe(() => {\n        if (reduced || pausedRef.current || cardCount < 2) return;\n        loopDelayRef.current?.kill();\n        loopDelayRef.current = gsap.delayedCall(PROCESS_ANIMATION.STEP_WAIT, () =>\n          runLoopRef.current()\n        );\n      });\n\n      const runLoop = contextSafe(() => {\n        if (reduced || pausedRef.current || cardCount < 2) return;\n        if (busyRef.current) {\n          scheduleLoop();\n          return;\n        }\n\n        const currentIndex = activeIndexRef.current;\n        const nextIndex = (currentIndex + 1) % cardCount;\n        const lineIndex = currentIndex % Math.max(cardCount - 1, 1);\n\n        lineTlRef.current?.kill();\n        lineTlRef.current = gsap.timeline({\n          onComplete: () => {\n            slideTo(nextIndex);\n            scheduleLoop();\n          },\n        });\n        animateTimelineLine(timelines, lineIndex, lineTlRef.current);\n      });\n\n      const runLoopRef = { current: runLoop };\n\n      goToStepRef.current = contextSafe((index: number) => {\n        if (index === activeIndexRef.current || busyRef.current) return;\n        lineTlRef.current?.kill();\n        loopDelayRef.current?.kill();\n        slideTo(index, index < activeIndexRef.current);\n        scheduleLoop();\n      });\n\n      pauseRef.current = contextSafe(() => {\n        pausedRef.current = true;\n        lineTlRef.current?.pause();\n        loopDelayRef.current?.pause();\n      });\n\n      resumeRef.current = contextSafe(() => {\n        pausedRef.current = false;\n        loopDelayRef.current?.kill();\n        if (lineTlRef.current?.paused()) {\n          lineTlRef.current.resume();\n          return;\n        }\n        if (!busyRef.current) runLoop();\n        else scheduleLoop();\n      });\n\n      if (!reduced && cardCount > 1) scheduleLoop();\n\n      return () => {\n        loopDelayRef.current?.kill();\n        lineTlRef.current?.kill();\n        slideTlRef.current?.kill();\n      };\n    },\n    { scope: rootRef, dependencies: [cardCount], revertOnUpdate: true }\n  );\n\n  const renderTimeline = (axis: 'horizontal' | 'vertical') => {\n    const isHorizontal = axis === 'horizontal';\n    const linesRef = isHorizontal ? horizontalLinesRef : verticalLinesRef;\n\n    return (\n      <div\n        data-process-timeline={axis}\n        className={cn(\n          'flex items-center justify-center',\n          isHorizontal\n            ? 'w-full flex-row md:hidden'\n            : 'hidden shrink-0 flex-col self-stretch md:flex'\n        )}\n      >\n        {steps.map((step, index) => (\n          <div\n            key={`${axis}-${step.id}`}\n            className={cn(\n              'flex items-center',\n              isHorizontal ? 'flex-row' : 'flex-col',\n              index < lineCount && 'flex-1'\n            )}\n          >\n            <button\n              type=\"button\"\n              data-item={getStepState(index)}\n              aria-label={`Go to step ${index + 1}: ${step.title}`}\n              aria-current={getStepState(index) === 'active' ? 'step' : undefined}\n              onClick={() => goToStepRef.current(index)}\n              className={cn(\n                'flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-full border text-xs font-medium transition-all duration-400 ease-in-out motion-reduce:transition-none',\n                'border-[#045f64]/20 text-[#045f64]/50 dark:border-[#045f64]/40 dark:text-[#9fd4d6]/80',\n                'data-[item=active]:border-transparent data-[item=active]:bg-[#045f64] data-[item=active]:text-white',\n                'data-[item=completed]:border-transparent data-[item=completed]:bg-[#045f64] data-[item=completed]:text-white',\n                'focus-visible:ring-2 focus-visible:ring-[#045f64] focus-visible:ring-offset-2 focus-visible:outline-none dark:ring-offset-[#12161F]'\n              )}\n            >\n              {String(index + 1).padStart(2, '0')}\n            </button>\n            {index < lineCount && (\n              <div\n                className={cn(\n                  'relative bg-[#045f64]/15 dark:bg-[#045f64]/30',\n                  isHorizontal ? 'mx-1 h-px min-w-8 flex-1' : 'mx-auto min-h-8 w-px flex-1'\n                )}\n              >\n                <div\n                  ref={(el) => {\n                    linesRef.current[index] = el;\n                  }}\n                  data-line-axis={axis}\n                  className={cn(\n                    'absolute inset-0',\n                    isHorizontal\n                      ? 'rounded-r-full bg-linear-to-r from-transparent to-[#c6f56f]'\n                      : 'rounded-b-full bg-linear-to-b from-transparent to-[#c6f56f]'\n                  )}\n                />\n              </div>\n            )}\n          </div>\n        ))}\n      </div>\n    );\n  };\n\n  return (\n    <section\n      ref={rootRef}\n      data-process-card-reveal\n      data-active-step={activeIndex}\n      className={cn('w-full', className)}\n      {...props}\n    >\n      <div className=\"mx-auto w-full max-w-[720px] px-3\">\n        <div\n          className=\"relative flex flex-col items-stretch justify-center gap-y-4 md:flex-row md:items-stretch md:gap-x-5\"\n          onMouseEnter={() => pauseRef.current()}\n          onMouseLeave={() => resumeRef.current()}\n        >\n          {renderTimeline('horizontal')}\n\n          <div className=\"relative min-h-[340px] w-full overflow-hidden md:h-[280px] md:min-h-0 md:flex-1\">\n            {steps.map((step, index) => (\n              <div\n                key={step.id}\n                ref={(el) => {\n                  cardsRef.current[index] = el;\n                }}\n                data-process-card\n                data-step-id={step.id}\n                className=\"absolute inset-0 rounded-2xl bg-white p-1 dark:bg-[#12161F]\"\n              >\n                <div className=\"flex h-full w-full flex-col items-stretch gap-y-3 md:flex-row md:gap-x-5\">\n                  <div className=\"w-full shrink-0 overflow-hidden rounded-xl md:w-[46%]\">\n                    <figure\n                      ref={(el) => {\n                        imagesRef.current[index] = el;\n                      }}\n                      className=\"relative h-48 w-full overflow-hidden rounded-xl md:h-full\"\n                    >\n                      <img\n                        src={step.image}\n                        alt={step.imageAlt}\n                        className=\"size-full rounded-xl object-cover\"\n                      />\n                      <figure\n                        className=\"absolute bottom-3 left-1/2 z-20 w-[calc(100%-24px)] -translate-x-1/2 overflow-hidden rounded-xl bg-white/15 backdrop-blur-[20px]\"\n                        style={overlayFigureStyle}\n                      >\n                        {step.overlay ? (\n                          <img src={step.overlay} alt={step.overlayAlt ?? ''} />\n                        ) : (\n                          <div className=\"px-3 py-2\">\n                            <p className=\"text-xs font-medium text-white drop-shadow\">\n                              {step.title}\n                            </p>\n                          </div>\n                        )}\n                      </figure>\n                    </figure>\n                  </div>\n\n                  <div className=\"relative min-w-0 flex-1 overflow-hidden py-2 md:py-3\">\n                    <div\n                      ref={(el) => {\n                        contentsRef.current[index] = el;\n                      }}\n                      className=\"h-fit w-full space-y-4 max-md:px-3 md:h-full md:justify-center\"\n                    >\n                      <div className=\"space-y-1\">\n                        <h3 className=\"text-base font-medium text-[#12161F] md:text-lg dark:text-white\">\n                          {step.title}\n                        </h3>\n                        <p className=\"text-left text-sm text-[#045f64]/70 dark:text-[#9fd4d6]/80\">\n                          {step.description}\n                        </p>\n                      </div>\n                      <div className=\"hidden space-y-3 md:block\">\n                        {step.details.map((detail) => (\n                          <div key={detail.title} className=\"space-y-0.5\">\n                            <h4 className=\"text-sm font-medium text-[#12161F] dark:text-white\">\n                              {detail.title}\n                            </h4>\n                            <p className=\"text-left text-sm text-[#045f64]/70 dark:text-[#9fd4d6]/80\">\n                              {detail.description}\n                            </p>\n                          </div>\n                        ))}\n                      </div>\n                    </div>\n                  </div>\n                </div>\n              </div>\n            ))}\n          </div>\n\n          {renderTimeline('vertical')}\n        </div>\n      </div>\n    </section>\n  );\n}\n"
    }
  ],
  "docs": "Docs, props and examples: https://tween-ui.vercel.app/block/process-card-reveal\nThe sample content loads images from images.unsplash.com. Replace them with your own before shipping."
}