{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"parallax-cards","type":"registry:block","title":"Parallax cards","description":"A reusable and responsive component that gives each of its children a smooth parallax effect, adding a modern, interactive touch to your website. Perfect for showcasing lists of content blocks, feature cards, or image galleries.","dependencies":["motion"],"files":[{"path":"registry/100xui/blocks/parallax-cards/components/parallax-cards.tsx","content":"\"use client\";\n\nimport React from \"react\";\nimport {\n  useScroll,\n  useTransform,\n  motion,\n  type MotionValue,\n  useMotionValueEvent,\n} from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface ParallaxCardsProps extends React.ComponentPropsWithoutRef<\"div\"> {\n  children: React.ReactElement[];\n  maxStackedCards?: number;\n  top?: React.CSSProperties[\"top\"];\n  forceParallax?: boolean;\n}\n\nexport function ParallaxCards({\n  maxStackedCards = 3,\n  top = \"50px\",\n  forceParallax = false,\n  className,\n  style,\n  children,\n  ...rest\n}: ParallaxCardsProps) {\n  const totalCards = children.length;\n  const topMagnitude = parseFloat(String(top));\n  const topUnit = String(top).slice(String(topMagnitude).length) || \"px\";\n\n  if (topUnit === \"%\")\n    throw new Error(\n      \"Invalid `top` value: percentages (%) are not supported by <ParallaxCards/>.\"\n    );\n  const containerRef = React.useRef<HTMLDivElement>(null);\n  const [isSticky, setIsSticky] = React.useState(true);\n\n  const { scrollYProgress } = useScroll({\n    target: containerRef,\n    offset: [\"start start\", \"end start\"],\n  });\n\n  React.useEffect(() => {\n    if (forceParallax) return;\n\n    const element = containerRef.current;\n    if (!element) return;\n\n    const handleResize = () => {\n      const cardHeight = element.getBoundingClientRect().height / totalCards;\n      const viewportHeight =\n        window.visualViewport?.height ?? window.innerHeight;\n      setIsSticky(viewportHeight >= cardHeight);\n    };\n\n    handleResize();\n    window.visualViewport?.addEventListener(\"resize\", handleResize);\n    window.addEventListener(\"resize\", handleResize);\n\n    return () => {\n      window.removeEventListener(\"resize\", handleResize);\n      window.visualViewport?.removeEventListener(\"resize\", handleResize);\n    };\n  }, [totalCards, forceParallax]);\n\n  return (\n    <>\n      <style>{`\n        html {\n          scroll-behavior: smooth;\n        }\n      `}</style>\n\n      <div\n        ref={containerRef}\n        className={cn(\"w-full\", className, \"!relative !grid !py-0\")}\n        style={{\n          ...style,\n          gridTemplateRows: `repeat(${totalCards}, 1fr)`,\n        }}\n        {...rest}\n      >\n        {children.map((child, index) => (\n          <ParallaxCard\n            key={`cards[${index}]`}\n            index={index}\n            scrollYProgress={scrollYProgress}\n            scrollRatio={1 / totalCards}\n            maxStackedCards={maxStackedCards}\n            top={{\n              magnitude: topMagnitude,\n              unit: topUnit,\n              absolute: topMagnitude + topUnit,\n            }}\n            isSticky={isSticky}\n            forceParallax={forceParallax}\n          >\n            {child}\n          </ParallaxCard>\n        ))}\n      </div>\n    </>\n  );\n}\n\ninterface CardProps {\n  index: number;\n  scrollYProgress: MotionValue<number>;\n  scrollRatio: number;\n  children: React.ReactElement;\n  isSticky: boolean;\n  maxStackedCards: number;\n  top: {\n    magnitude: number;\n    unit: string;\n    absolute: string;\n  };\n  forceParallax: boolean;\n}\n\nfunction ParallaxCard({\n  index,\n  scrollYProgress,\n  maxStackedCards,\n  scrollRatio,\n  children,\n  top,\n  isSticky,\n  forceParallax,\n}: CardProps) {\n  const y = useTransform(\n    scrollYProgress,\n    [\n      index * scrollRatio,\n      (index + maxStackedCards - 1) * scrollRatio,\n      (index + maxStackedCards) * scrollRatio,\n    ],\n    [\n      \"0\",\n      `-${top.absolute}`,\n      `${-top.magnitude - top.magnitude / (maxStackedCards - 1)}${top.unit}`,\n    ]\n  );\n\n  const scale = useTransform(\n    scrollYProgress,\n    [index * scrollRatio, (index + maxStackedCards) * scrollRatio],\n    [1, 0.85]\n  );\n\n  // const opacity = useTransform(\n  //   scrollYProgress,\n\n  //   [\n  //     (index + maxStackedCards - 1) * scrollRatio,\n  //     (index + maxStackedCards) * scrollRatio,\n  //   ],\n  //   [1, 0],\n  // );\n\n  // useMotionValueEvent(opacity, \"change\", (value) => {\n  //   console.log(value, typeof value);\n  // });\n  return (\n    <div\n      style={{\n        paddingTop: top.absolute,\n        position: isSticky ? \"sticky\" : \"relative\",\n        top: \"0px\",\n      }}\n    >\n      <motion.div\n        {...(isSticky && {\n          style: {\n            scale,\n            // opacity,\n            y,\n            maxHeight: forceParallax ? `calc(100vh - ${top.absolute})` : \"none\",\n          },\n        })}\n        className=\"grid size-full origin-top overflow-hidden\"\n      >\n        {children}\n      </motion.div>\n    </div>\n  );\n}\n","type":"registry:component"}]}