{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A TiltCard component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport {\n  motion,\n  useMotionTemplate,\n  useMotionValue,\n  useReducedMotion,\n  useSpring,\n} from \"motion/react\";\nimport {\n  type ReactNode,\n  type PointerEvent as ReactPointerEvent,\n  useEffect,\n  useRef,\n  useState,\n} from \"react\";\n\nconst DEFAULT_MAX_TILT = 12;\nconst DEFAULT_PERSPECTIVE = 900;\nconst DEFAULT_SCALE = 1.03;\nconst DEFAULT_GLARE_OPACITY = 0.25;\nconst SPRING_DURATION = 0.25;\nconst SPRING_BOUNCE = 0.1;\nconst FOCUS_TILT_RATIO = 0.35;\nconst PARALLAX_STRENGTH = 24;\nconst GLARE_CENTER_PERCENT = 50;\nconst NORMALIZED_RANGE = 100;\n\nexport interface TiltCardProps {\n  children: ReactNode;\n  className?: string;\n  glare?: boolean;\n  glareOpacity?: number;\n  maxTilt?: number;\n  parallax?: boolean;\n  perspective?: number;\n  scale?: number;\n}\n\nconst useHoverCapable = () => {\n  const [isHoverCapable, setIsHoverCapable] = useState(false);\n\n  useEffect(() => {\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  }, []);\n\n  return isHoverCapable;\n};\n\nconst applyParallax = (\n  container: HTMLElement,\n  normalizedX: number,\n  normalizedY: number\n) => {\n  const depthElements =\n    container.querySelectorAll<HTMLElement>(\"[data-tilt-depth]\");\n\n  for (const element of depthElements) {\n    const depth = Number.parseFloat(element.dataset.tiltDepth ?? \"0\");\n    const boundedDepth = Number.isFinite(depth)\n      ? Math.min(1, Math.max(0, depth))\n      : 0;\n    const translateX = normalizedX * boundedDepth * PARALLAX_STRENGTH;\n    const translateY = normalizedY * boundedDepth * PARALLAX_STRENGTH;\n    element.style.transform = `translate3d(${translateX}px, ${translateY}px, 0)`;\n  }\n};\n\nconst resetParallax = (container: HTMLElement) => {\n  const depthElements =\n    container.querySelectorAll<HTMLElement>(\"[data-tilt-depth]\");\n  for (const element of depthElements) {\n    element.style.transform = \"translate3d(0, 0, 0)\";\n  }\n};\n\nexport default function TiltCard({\n  children,\n  className,\n  glare = true,\n  glareOpacity = DEFAULT_GLARE_OPACITY,\n  maxTilt = DEFAULT_MAX_TILT,\n  parallax = false,\n  perspective = DEFAULT_PERSPECTIVE,\n  scale = DEFAULT_SCALE,\n}: TiltCardProps) {\n  const shouldReduceMotion = Boolean(useReducedMotion());\n  const isHoverCapable = useHoverCapable();\n  const isInteractive = isHoverCapable && !shouldReduceMotion;\n\n  const containerRef = useRef<HTMLDivElement>(null);\n  const rotateX = useMotionValue(0);\n  const rotateY = useMotionValue(0);\n  const scaleValue = useMotionValue(1);\n  const glareX = useMotionValue(GLARE_CENTER_PERCENT);\n  const glareY = useMotionValue(GLARE_CENTER_PERCENT);\n  const glareStrength = useMotionValue(0);\n\n  const springConfig = { bounce: SPRING_BOUNCE, duration: SPRING_DURATION };\n  const springRotateX = useSpring(rotateX, springConfig);\n  const springRotateY = useSpring(rotateY, springConfig);\n  const springScale = useSpring(scaleValue, springConfig);\n  const springGlareX = useSpring(glareX, springConfig);\n  const springGlareY = useSpring(glareY, springConfig);\n  const springGlareStrength = useSpring(glareStrength, springConfig);\n\n  const glareBackground = useMotionTemplate`radial-gradient(circle at ${springGlareX}% ${springGlareY}%, rgba(255,255,255,${springGlareStrength}), transparent 60%)`;\n\n  useEffect(() => {\n    if (parallax) {\n      return;\n    }\n    const container = containerRef.current;\n    if (container) {\n      resetParallax(container);\n    }\n  }, [parallax]);\n\n  const handlePointerMove = (event: ReactPointerEvent<HTMLDivElement>) => {\n    if (!isInteractive) {\n      return;\n    }\n\n    const container = containerRef.current;\n    if (!container) {\n      return;\n    }\n\n    const rect = container.getBoundingClientRect();\n    const normalizedX = ((event.clientX - rect.left) / rect.width) * 2 - 1;\n    const normalizedY = ((event.clientY - rect.top) / rect.height) * 2 - 1;\n\n    rotateY.set(normalizedX * maxTilt);\n    rotateX.set(-normalizedY * maxTilt);\n    scaleValue.set(scale);\n    glareX.set(GLARE_CENTER_PERCENT + normalizedX * (NORMALIZED_RANGE / 2));\n    glareY.set(GLARE_CENTER_PERCENT + normalizedY * (NORMALIZED_RANGE / 2));\n    glareStrength.set(glareOpacity);\n\n    if (parallax) {\n      applyParallax(container, normalizedX, normalizedY);\n    }\n  };\n\n  const handlePointerLeave = () => {\n    if (!isInteractive) {\n      return;\n    }\n\n    rotateX.set(0);\n    rotateY.set(0);\n    scaleValue.set(1);\n    glareStrength.set(0);\n\n    const container = containerRef.current;\n    if (container && parallax) {\n      resetParallax(container);\n    }\n  };\n\n  const handleFocus = () => {\n    if (!isInteractive) {\n      return;\n    }\n\n    rotateX.set(maxTilt * FOCUS_TILT_RATIO);\n    rotateY.set(-maxTilt * FOCUS_TILT_RATIO);\n    scaleValue.set(scale);\n  };\n\n  const handleBlur = () => {\n    if (!isInteractive) {\n      return;\n    }\n\n    rotateX.set(0);\n    rotateY.set(0);\n    scaleValue.set(1);\n  };\n\n  const interactiveHandlers = isInteractive\n    ? {\n        onBlur: handleBlur,\n        onFocus: handleFocus,\n        onPointerLeave: handlePointerLeave,\n        onPointerMove: handlePointerMove,\n      }\n    : {};\n\n  return (\n    <motion.div\n      className={cn(\"relative\", className)}\n      ref={containerRef}\n      style={{\n        rotateX: springRotateX,\n        rotateY: springRotateY,\n        scale: springScale,\n        transformPerspective: perspective,\n        transformStyle: \"preserve-3d\",\n      }}\n      {...interactiveHandlers}\n    >\n      {children}\n      {glare && isInteractive ? (\n        <motion.div\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute inset-0 rounded-[inherit]\"\n          style={{ backgroundImage: glareBackground }}\n        />\n      ) : null}\n    </motion.div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/tilt-card/index.tsx","type":"registry:ui"}],"name":"tilt-card","registryDependencies":[],"title":"Tilt Card","type":"registry:ui"}