{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Multi-layer depth parallax for hero sections, with optional pointer-driven parallax.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport {\n  type MotionValue,\n  motion,\n  useMotionValue,\n  useReducedMotion,\n  useScroll,\n  useSpring,\n  useTransform,\n} from \"motion/react\";\nimport {\n  type ReactNode,\n  type RefObject,\n  useEffect,\n  useRef,\n  useState,\n} from \"react\";\n\n/** Motion scroll offset tuple, typed from `useScroll` itself. */\nexport type ScrollOffset = NonNullable<\n  Parameters<typeof useScroll>[0]\n>[\"offset\"];\n\nconst SCROLL_SPRING_CONFIG = {\n  damping: 30,\n  mass: 0.5,\n  stiffness: 200,\n} as const;\nconst POINTER_SPRING_CONFIG = {\n  damping: 20,\n  mass: 0.3,\n  stiffness: 150,\n} as const;\nconst POINTER_PARALLAX_MAX_PX = 20;\nconst DEFAULT_RANGE_PX = 80;\n// Concentrated on the stretch where the scene is actually on screen: over the\n// full enter-to-exit range most of the layer separation is spent off-view.\nconst DEFAULT_OFFSET: ScrollOffset = [\"start 0.95\", \"end 0.6\"];\nconst CENTER_RATIO = 0.5;\n\nexport type ParallaxDirection = \"vertical\" | \"horizontal\" | \"both\";\n\nexport interface ParallaxLayer {\n  /** Optional blur, in pixels, applied to this layer for extra depth. */\n  blur?: number;\n  content: ReactNode;\n  /** 0 (no movement) to 1 (maximum movement). */\n  depth: number;\n  id: string;\n  opacity?: number;\n}\n\nexport interface ParallaxLayersProps {\n  className?: string;\n  /** Ref to a scrollable ancestor that drives progress instead of the window. */\n  container?: RefObject<HTMLElement | null>;\n  direction?: ParallaxDirection;\n  layers: ParallaxLayer[];\n  /** Also react to pointer position on hover-capable devices. */\n  pointerParallax?: boolean;\n  /** Maximum travel, in pixels, at depth 1. */\n  range?: number;\n}\n\nconst useHoverCapable = (enabled: boolean) => {\n  const [isHoverCapable, setIsHoverCapable] = useState(false);\n\n  useEffect(() => {\n    if (!enabled) {\n      return;\n    }\n\n    const mediaQuery = window.matchMedia(\"(hover: hover) and (pointer: fine)\");\n    setIsHoverCapable(mediaQuery.matches);\n\n    const handleChange = (event: MediaQueryListEvent) => {\n      setIsHoverCapable(event.matches);\n    };\n\n    mediaQuery.addEventListener(\"change\", handleChange);\n    return () => mediaQuery.removeEventListener(\"change\", handleChange);\n  }, [enabled]);\n\n  return isHoverCapable;\n};\n\ninterface ParallaxLayerItemProps {\n  direction: ParallaxDirection;\n  layer: ParallaxLayer;\n  pointerX: MotionValue<number>;\n  pointerY: MotionValue<number>;\n  progress: MotionValue<number>;\n  range: number;\n}\n\nconst ParallaxLayerItem = ({\n  layer,\n  progress,\n  pointerX,\n  pointerY,\n  direction,\n  range,\n}: ParallaxLayerItemProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const { depth, blur, opacity } = layer;\n\n  const scrollX = useTransform(\n    progress,\n    [0, 1],\n    [-range * depth, range * depth]\n  );\n  const scrollY = useTransform(\n    progress,\n    [0, 1],\n    [-range * depth, range * depth]\n  );\n  const pointerOffsetX = useTransform(pointerX, (latest) => latest * depth);\n  const pointerOffsetY = useTransform(pointerY, (latest) => latest * depth);\n\n  const x = useTransform([scrollX, pointerOffsetX], (latest) => {\n    const [scrollValue, pointerValue] = latest as [number, number];\n    return shouldReduceMotion || direction === \"vertical\"\n      ? 0\n      : scrollValue + pointerValue;\n  });\n  const y = useTransform([scrollY, pointerOffsetY], (latest) => {\n    const [scrollValue, pointerValue] = latest as [number, number];\n    return shouldReduceMotion || direction === \"horizontal\"\n      ? 0\n      : scrollValue + pointerValue;\n  });\n\n  return (\n    <motion.div\n      className=\"absolute inset-0\"\n      style={{\n        filter: blur ? `blur(${blur}px)` : undefined,\n        opacity: opacity ?? 1,\n        x,\n        y,\n      }}\n    >\n      {layer.content}\n    </motion.div>\n  );\n};\n\nexport default function ParallaxLayers({\n  layers,\n  direction = \"vertical\",\n  range = DEFAULT_RANGE_PX,\n  pointerParallax = false,\n  container,\n  className,\n}: ParallaxLayersProps) {\n  const ref = useRef<HTMLDivElement>(null);\n  const shouldReduceMotion = useReducedMotion();\n  const isHoverCapable = useHoverCapable(pointerParallax);\n\n  const { scrollYProgress } = useScroll({\n    container,\n    offset: DEFAULT_OFFSET,\n    target: ref,\n  });\n  const springProgress = useSpring(scrollYProgress, SCROLL_SPRING_CONFIG);\n\n  const pointerXRaw = useMotionValue(0);\n  const pointerYRaw = useMotionValue(0);\n  const pointerX = useSpring(pointerXRaw, POINTER_SPRING_CONFIG);\n  const pointerY = useSpring(pointerYRaw, POINTER_SPRING_CONFIG);\n\n  useEffect(() => {\n    const node = ref.current;\n    const isPointerActive =\n      pointerParallax && isHoverCapable && !shouldReduceMotion;\n\n    if (!(node && isPointerActive)) {\n      return;\n    }\n\n    const handlePointerMove = (event: PointerEvent) => {\n      const rect = node.getBoundingClientRect();\n      const relativeX = (event.clientX - rect.left) / rect.width - CENTER_RATIO;\n      const relativeY = (event.clientY - rect.top) / rect.height - CENTER_RATIO;\n      pointerXRaw.set(relativeX * POINTER_PARALLAX_MAX_PX);\n      pointerYRaw.set(relativeY * POINTER_PARALLAX_MAX_PX);\n    };\n\n    const handlePointerLeave = () => {\n      pointerXRaw.set(0);\n      pointerYRaw.set(0);\n    };\n\n    node.addEventListener(\"pointermove\", handlePointerMove);\n    node.addEventListener(\"pointerleave\", handlePointerLeave);\n\n    return () => {\n      node.removeEventListener(\"pointermove\", handlePointerMove);\n      node.removeEventListener(\"pointerleave\", handlePointerLeave);\n    };\n  }, [\n    pointerParallax,\n    isHoverCapable,\n    shouldReduceMotion,\n    pointerXRaw,\n    pointerYRaw,\n  ]);\n\n  return (\n    <div className={cn(\"relative\", className)} ref={ref}>\n      {layers.map((layer) => (\n        <ParallaxLayerItem\n          direction={direction}\n          key={layer.id}\n          layer={layer}\n          pointerX={pointerX}\n          pointerY={pointerY}\n          progress={springProgress}\n          range={range}\n        />\n      ))}\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/parallax-layers/index.tsx","type":"registry:ui"}],"name":"parallax-layers","registryDependencies":[],"title":"Parallax Layers","type":"registry:ui"}