{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"parallax-card","title":"Parallax Card","description":"A generic card with pointer-driven tilt, glare, and parallax motion.","files":[{"path":"ui/parallax-card.tsx","type":"registry:ui","content":"\"use client\";\n\nimport {\n  type HTMLMotionProps,\n  motion,\n  type MotionStyle,\n  useMotionTemplate,\n  useMotionValue,\n  useSpring,\n  useTransform,\n} from \"motion/react\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\ntype SpringConfig = {\n  stiffness: number;\n  damping: number;\n  mass: number;\n};\n\ntype ParallaxCardProps = Omit<HTMLMotionProps<\"div\">, \"children\"> & {\n  children: React.ReactNode;\n  contentClassName?: string;\n  contentStyle?: MotionStyle;\n  glareClassName?: string;\n  disabled?: boolean;\n  maxTilt?: number;\n  perspective?: number;\n  scale?: number;\n  glare?: boolean;\n  glareMaxOpacity?: number;\n  parallaxShift?: number;\n  spring?: Partial<SpringConfig>;\n};\n\nconst defaultSpring = {\n  stiffness: 300,\n  damping: 30,\n  mass: 0.5,\n} satisfies SpringConfig;\n\nfunction ParallaxCard({\n  children,\n  className,\n  contentClassName,\n  contentStyle,\n  glareClassName,\n  disabled = false,\n  maxTilt = 8,\n  perspective = 800,\n  scale: hoverScale = 1.02,\n  glare = true,\n  glareMaxOpacity = 0.16,\n  parallaxShift = 6,\n  spring,\n  style,\n  onMouseEnter,\n  onMouseLeave,\n  onMouseMove,\n  ...props\n}: ParallaxCardProps) {\n  const cardRef = React.useRef<HTMLDivElement>(null);\n  const rectRef = React.useRef<DOMRect | null>(null);\n  const [prefersStaticCard, setPrefersStaticCard] = React.useState(false);\n  const stiffness = spring?.stiffness ?? defaultSpring.stiffness;\n  const damping = spring?.damping ?? defaultSpring.damping;\n  const mass = spring?.mass ?? defaultSpring.mass;\n\n  React.useEffect(() => {\n    const reducedMotion = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    const finePointer = window.matchMedia(\"(pointer: fine)\");\n    const updatePreference = () => {\n      setPrefersStaticCard(reducedMotion.matches || !finePointer.matches);\n    };\n\n    updatePreference();\n    reducedMotion.addEventListener(\"change\", updatePreference);\n    finePointer.addEventListener(\"change\", updatePreference);\n\n    return () => {\n      reducedMotion.removeEventListener(\"change\", updatePreference);\n      finePointer.removeEventListener(\"change\", updatePreference);\n    };\n  }, []);\n\n  const springConfig = React.useMemo(\n    () => ({ stiffness, damping, mass }),\n    [stiffness, damping, mass],\n  );\n  const mouseX = useMotionValue(0.5);\n  const mouseY = useMotionValue(0.5);\n  const isHovered = useMotionValue(0);\n\n  const rawRotateY = useTransform(mouseX, [0, 1], [maxTilt, -maxTilt]);\n  const rawRotateX = useTransform(mouseY, [0, 1], [-maxTilt, maxTilt]);\n  const rawScale = useTransform(isHovered, [0, 1], [1, hoverScale]);\n  const rotateX = useSpring(rawRotateX, springConfig);\n  const rotateY = useSpring(rawRotateY, springConfig);\n  const scale = useSpring(rawScale, springConfig);\n  const hoverOpacity = useSpring(isHovered, springConfig);\n  const contentX = useSpring(\n    useTransform(mouseX, [0, 1], [parallaxShift, -parallaxShift]),\n    springConfig,\n  );\n  const contentY = useSpring(\n    useTransform(mouseY, [0, 1], [parallaxShift, -parallaxShift]),\n    springConfig,\n  );\n  const glareX = useTransform(mouseX, [0, 1], [0, 100]);\n  const glareY = useTransform(mouseY, [0, 1], [0, 100]);\n  const glareOpacity = useTransform(hoverOpacity, [0, 1], [0, glareMaxOpacity]);\n  const glareBackground = useMotionTemplate`radial-gradient(circle at ${glareX}% ${glareY}%, rgba(255, 255, 255, 0.38) 0%, transparent 58%)`;\n  const isStatic = disabled || prefersStaticCard;\n  const cardMotionStyle: MotionStyle = isStatic\n    ? {}\n    : { transformPerspective: perspective, rotateX, rotateY, scale };\n  const contentMotionStyle: MotionStyle = isStatic ? {} : { x: contentX, y: contentY };\n\n  function handleMouseEnter(event: React.MouseEvent<HTMLDivElement>) {\n    onMouseEnter?.(event);\n\n    if (isStatic) {\n      return;\n    }\n\n    rectRef.current = cardRef.current?.getBoundingClientRect() ?? null;\n    isHovered.set(1);\n  }\n\n  function handleMouseMove(event: React.MouseEvent<HTMLDivElement>) {\n    onMouseMove?.(event);\n\n    if (isStatic || !rectRef.current) {\n      return;\n    }\n\n    const rect = rectRef.current;\n    const x = (event.clientX - rect.left) / rect.width;\n    const y = (event.clientY - rect.top) / rect.height;\n\n    mouseX.set(Math.max(0, Math.min(1, x)));\n    mouseY.set(Math.max(0, Math.min(1, y)));\n  }\n\n  function handleMouseLeave(event: React.MouseEvent<HTMLDivElement>) {\n    onMouseLeave?.(event);\n\n    if (isStatic) {\n      return;\n    }\n\n    mouseX.set(0.5);\n    mouseY.set(0.5);\n    isHovered.set(0);\n    rectRef.current = null;\n  }\n\n  return (\n    <motion.div\n      ref={cardRef}\n      data-slot=\"parallax-card\"\n      className={cn(\n        \"relative block overflow-hidden rounded-xl border bg-card text-card-foreground shadow-sm outline-none\",\n        className,\n      )}\n      style={{ ...style, transformStyle: \"preserve-3d\", ...cardMotionStyle }}\n      onMouseEnter={handleMouseEnter}\n      onMouseMove={handleMouseMove}\n      onMouseLeave={handleMouseLeave}\n      {...props}\n    >\n      <motion.div\n        data-slot=\"parallax-card-content\"\n        className={cn(\"relative z-10 h-full scale-[1.03] will-change-transform\", contentClassName)}\n        style={{ ...contentStyle, ...contentMotionStyle }}\n      >\n        {children}\n      </motion.div>\n      {glare && (\n        <motion.div\n          aria-hidden=\"true\"\n          data-slot=\"parallax-card-glare\"\n          className={cn(\n            \"pointer-events-none absolute inset-0 z-20 rounded-[inherit]\",\n            glareClassName,\n          )}\n          style={{ background: glareBackground, opacity: isStatic ? 0 : glareOpacity }}\n        />\n      )}\n    </motion.div>\n  );\n}\n\nexport { ParallaxCard };","target":"@ui/parallax-card.tsx"}],"type":"registry:ui","dependencies":["motion"]}