{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tilt-card",
  "type": "registry:ui",
  "title": "Tilt Card",
  "description": "A perspective card that tilts toward the pointer, with a spring-smoothed glare and layered depth.",
  "author": "WANSTUDIO <https://wanstudio.my>",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/wancraft/tilt-card.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { motion, useMotionTemplate, useMotionValue, useSpring } from \"motion/react\";\n\n/* SSR-safe: renders \"no preference\" on the server and the first client pass, so markup always hydrates. */\nfunction subscribeReducedMotion(onChange: () => void) {\n  const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n  mq.addEventListener(\"change\", onChange);\n  return () => mq.removeEventListener(\"change\", onChange);\n}\n\nfunction usePrefersReducedMotion() {\n  return React.useSyncExternalStore(\n    subscribeReducedMotion,\n    () => window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches,\n    () => false,\n  );\n}\nexport interface TiltCardProps extends React.ComponentPropsWithoutRef<\"div\"> {\n  /** Maximum rotation in degrees at the card's edges. */\n  tiltStrength?: number;\n  /** Show a light reflection that follows the pointer. */\n  glare?: boolean;\n  /** Peak opacity of the glare, 0–1. */\n  glareOpacity?: number;\n  /** Spring that smooths rotation, lift and glare. */\n  spring?: { stiffness: number; damping: number; mass: number };\n  /** Scale while hovered (1 = no lift). */\n  scale?: number;\n  /** CSS perspective distance in px — lower is more dramatic. */\n  perspective?: number;\n  /** Corner radius of the surface in px. */\n  rounded?: number;\n  /** Classes for the tilting surface (background, border, shadow). Replaces the default surface styling. */\n  surfaceClassName?: string;\n}\n\nconst DEFAULT_SURFACE =\n  \"border border-zinc-900/10 bg-white shadow-[0_1px_0_rgb(255_255_255/0.6)_inset,0_24px_48px_-24px_rgb(24_26_28/0.35)] dark:border-white/10 dark:bg-[#1b1f23] dark:shadow-[0_1px_0_rgb(255_255_255/0.05)_inset,0_24px_48px_-20px_rgb(0_0_0/0.7)]\";\n\n/**\n * A card that tilts toward the pointer in 3D with an optional glare.\n * Children can use `[transform:translateZ(32px)]` to float above the surface.\n * Touch input and reduced-motion preferences get a flat, static card.\n */\nexport function TiltCard({\n  tiltStrength = 12,\n  glare = true,\n  glareOpacity = 0.35,\n  spring = { stiffness: 220, damping: 22, mass: 0.6 },\n  scale = 1.03,\n  perspective = 900,\n  rounded = 20,\n  surfaceClassName = DEFAULT_SURFACE,\n  className = \"\",\n  style,\n  children,\n  onPointerMove,\n  onPointerEnter,\n  onPointerLeave,\n  ...props\n}: TiltCardProps) {\n  const reduceMotion = usePrefersReducedMotion();\n\n  // Raw targets, set directly from pointer events (no React state).\n  const rotateXTarget = useMotionValue(0);\n  const rotateYTarget = useMotionValue(0);\n  const scaleTarget = useMotionValue(1);\n  const glareTarget = useMotionValue(0);\n  const glareX = useMotionValue(50);\n  const glareY = useMotionValue(50);\n\n  const rotateX = useSpring(rotateXTarget, spring);\n  const rotateY = useSpring(rotateYTarget, spring);\n  const lift = useSpring(scaleTarget, spring);\n  const glareAlpha = useSpring(glareTarget, spring);\n  const glareBackground = useMotionTemplate`radial-gradient(circle at ${glareX}% ${glareY}%, rgb(255 255 255 / 0.9) 0%, rgb(255 255 255 / 0.25) 28%, rgb(255 255 255 / 0) 62%)`;\n\n  const enabled = !reduceMotion;\n\n  const track = (event: React.PointerEvent<HTMLDivElement>) => {\n    // Only fine pointers tilt; touch scrolling must never fight the card.\n    if (!enabled || event.pointerType === \"touch\") return;\n    const rect = event.currentTarget.getBoundingClientRect();\n    const px = (event.clientX - rect.left) / rect.width - 0.5; // -0.5 … 0.5\n    const py = (event.clientY - rect.top) / rect.height - 0.5;\n    rotateYTarget.set(px * 2 * tiltStrength);\n    rotateXTarget.set(-py * 2 * tiltStrength);\n    scaleTarget.set(scale);\n    glareX.set((px + 0.5) * 100);\n    glareY.set((py + 0.5) * 100);\n    glareTarget.set(glare ? Math.min(Math.max(glareOpacity, 0), 1) : 0);\n  };\n\n  const reset = () => {\n    rotateXTarget.set(0);\n    rotateYTarget.set(0);\n    scaleTarget.set(1);\n    glareTarget.set(0);\n  };\n\n  return (\n    <div\n      className={`relative inline-block ${className}`}\n      style={{ perspective, ...style }}\n      onPointerEnter={(event) => {\n        onPointerEnter?.(event);\n        track(event);\n      }}\n      onPointerMove={(event) => {\n        onPointerMove?.(event);\n        track(event);\n      }}\n      onPointerLeave={(event) => {\n        onPointerLeave?.(event);\n        reset();\n      }}\n      {...props}\n    >\n      <motion.div\n        className={`relative h-full w-full has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-4 has-[:focus-visible]:outline-zinc-400 ${surfaceClassName}`}\n        style={{\n          borderRadius: rounded,\n          rotateX,\n          rotateY,\n          scale: lift,\n          transformStyle: \"preserve-3d\",\n        }}\n      >\n        {children}\n        {glare && enabled && (\n          <span\n            aria-hidden=\"true\"\n            className=\"pointer-events-none absolute inset-0 overflow-hidden\"\n            style={{ borderRadius: \"inherit\", transform: \"translateZ(1px)\" }}\n          >\n            <motion.span\n              className=\"absolute inset-0\"\n              style={{ background: glareBackground, opacity: glareAlpha }}\n            />\n          </span>\n        )}\n      </motion.div>\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "cards"
  ],
  "docs": "Documentation: https://wancraft.my/components/tilt-card"
}
