{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tilt-card",
  "title": "Tilt Card",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A card that tilts in 3D toward the pointer with spring smoothing and a moving glare. TiltCardLayer children float at their own depth for parallax. Keyboard focus shows a gentle tilt.",
  "dependencies": [
    "motion@^12"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/motion-presets.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/tilt-card.tsx",
      "content": "// Ballmac UI: Tilt Card. https://ui.ballmac.com/components/tilt-card\n\"use client\"\n\nimport * as React from \"react\"\nimport { motion, type MotionStyle, useMotionTemplate, useMotionValue, useSpring, useTransform } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useReducedMotionSafe } from \"@/lib/ballmac/motion\"\n\ntype TiltCardProps = Omit<\n  React.ComponentProps<\"div\">,\n  \"onDrag\" | \"onDragStart\" | \"onDragEnd\" | \"onAnimationStart\" | \"style\"\n> & {\n  /** Largest rotation on each axis, in degrees. */\n  maxTilt?: number\n  /** Distance of the viewer in pixels. Smaller values exaggerate depth. */\n  perspective?: number\n  /** Scale while the pointer is over the card. */\n  hoverScale?: number\n  /** Draw a soft light highlight that follows the pointer. */\n  glare?: boolean\n  /** Peak opacity of the glare, 0 to 1. */\n  glareOpacity?: number\n  /** Inline styles for the card. */\n  style?: React.CSSProperties\n}\n\nconst SPRING = { stiffness: 220, damping: 22, mass: 0.6 }\n\nfunction isFocusVisible(element: Element) {\n  try {\n    return element.matches(\":focus-visible\")\n  } catch {\n    return false\n  }\n}\n\nfunction TiltCard({\n  maxTilt = 10,\n  perspective = 900,\n  hoverScale = 1.02,\n  glare = true,\n  glareOpacity = 0.28,\n  className,\n  style,\n  children,\n  onPointerMove,\n  onPointerEnter,\n  onPointerLeave,\n  onFocus,\n  onBlur,\n  ...props\n}: TiltCardProps) {\n  const reduceMotion = useReducedMotionSafe()\n  // Pointer position over the card, 0–1 on each axis; 0.5 is the center.\n  const px = useMotionValue(0.5)\n  const py = useMotionValue(0.5)\n  const active = useMotionValue(0)\n  const sx = useSpring(px, SPRING)\n  const sy = useSpring(py, SPRING)\n  const sActive = useSpring(active, SPRING)\n\n  const rotateX = useTransform(sy, [0, 1], [maxTilt, -maxTilt])\n  const rotateY = useTransform(sx, [0, 1], [-maxTilt, maxTilt])\n  const scale = useTransform(sActive, [0, 1], [1, hoverScale])\n  const glareX = useTransform(sx, (v) => `${v * 100}%`)\n  const glareY = useTransform(sy, (v) => `${v * 100}%`)\n  const glareAlpha = useTransform(sActive, (v) => v * glareOpacity)\n  const glareBackground = useMotionTemplate`radial-gradient(farthest-corner circle at ${glareX} ${glareY}, rgb(255 255 255 / ${glareAlpha}), transparent 62%)`\n\n  const moveTo = (x: number, y: number, on: number) => {\n    px.set(x)\n    py.set(y)\n    active.set(on)\n  }\n  const reset = () => moveTo(0.5, 0.5, 0)\n\n  return (\n    <motion.div\n      data-slot=\"tilt-card\"\n      className={cn(\n        \"group/tilt relative rounded-xl border bg-card text-card-foreground outline-none transform-3d focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n        className\n      )}\n      style={\n        reduceMotion\n          ? (style as MotionStyle)\n          : ({\n              ...style,\n              rotateX,\n              rotateY,\n              scale,\n              transformPerspective: perspective,\n              // Exposed for custom effects (holographic foil, parallax gradients): 0% to 100%.\n              \"--tilt-x\": glareX,\n              \"--tilt-y\": glareY,\n            } as MotionStyle)\n      }\n      onPointerEnter={(event) => {\n        if (!reduceMotion && event.pointerType !== \"touch\") active.set(1)\n        onPointerEnter?.(event)\n      }}\n      onPointerMove={(event) => {\n        if (!reduceMotion && event.pointerType !== \"touch\") {\n          const rect = event.currentTarget.getBoundingClientRect()\n          moveTo((event.clientX - rect.left) / rect.width, (event.clientY - rect.top) / rect.height, 1)\n        }\n        onPointerMove?.(event)\n      }}\n      onPointerLeave={(event) => {\n        reset()\n        onPointerLeave?.(event)\n      }}\n      onFocus={(event) => {\n        // Keyboard focus gets a gentle, fixed tilt toward the top-right corner.\n        if (!reduceMotion && isFocusVisible(event.target)) moveTo(0.68, 0.36, 0.7)\n        onFocus?.(event)\n      }}\n      onBlur={(event) => {\n        if (!event.currentTarget.contains(event.relatedTarget as Node | null)) reset()\n        onBlur?.(event)\n      }}\n      {...props}\n    >\n      {children}\n      {glare && !reduceMotion && (\n        <motion.span\n          aria-hidden=\"true\"\n          data-slot=\"tilt-card-glare\"\n          className=\"pointer-events-none absolute inset-0 z-10 rounded-[inherit] mix-blend-soft-light\"\n          style={{ background: glareBackground }}\n        />\n      )}\n      {glare && !reduceMotion && (\n        <motion.span\n          aria-hidden=\"true\"\n          data-slot=\"tilt-card-sheen\"\n          className=\"pointer-events-none absolute inset-0 z-10 rounded-[inherit]\"\n          style={{ background: glareBackground, opacity: 0.6 }}\n        />\n      )}\n    </motion.div>\n  )\n}\n\ntype TiltCardLayerProps = React.ComponentProps<\"div\"> & {\n  /** How far the layer floats above the card, in pixels. Larger values move more with the tilt. */\n  depth?: number\n}\n\nfunction TiltCardLayer({ depth = 24, className, style, ...props }: TiltCardLayerProps) {\n  return (\n    <div\n      data-slot=\"tilt-card-layer\"\n      className={cn(\"transform-3d\", className)}\n      style={{ transform: `translateZ(${depth}px)`, ...style }}\n      {...props}\n    />\n  )\n}\n\nexport { TiltCard, TiltCardLayer, type TiltCardProps, type TiltCardLayerProps }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/tilt-card.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "card",
      "3d",
      "tilt",
      "parallax",
      "hover",
      "glare",
      "motion"
    ],
    "version": "1.0.0",
    "updated": "2026-09-29",
    "ai": {
      "summary": "<TiltCard> is a card surface (rounded-xl, border, bg-card) that rotates toward the pointer; wrap parts in <TiltCardLayer depth={24}> to lift them off the surface. The card sets --tilt-x and --tilt-y (0%–100%) for custom foil or gradient effects.",
      "whenToUse": [
        "A single hero object: a membership or gift card, a product box, an app icon",
        "Pricing or product cards on a landing page that should feel physical",
        "Collectible or badge-style achievements"
      ],
      "whenNotToUse": [
        "Dense grids of many cards (the motion competes; use spotlight-card)",
        "Cards holding forms or text people need to read while moving the pointer"
      ],
      "composesWith": [
        "spotlight-card",
        "border-beam"
      ],
      "a11y": [
        {
          "keys": "Tab",
          "action": "Focusing the card (tabIndex) or a link inside it shows a gentle fixed tilt"
        }
      ],
      "customization": [
        "maxTilt in degrees (default 10), perspective in px (default 900), hoverScale (default 1.02)",
        "glare, glareOpacity (default 0.28)",
        "TiltCardLayer depth in px; parents of layers need transform-3d (the card already has it)",
        "Clip backgrounds in an inner absolute div with overflow-hidden rounded-[inherit]; overflow-hidden on the card itself flattens the 3D layers",
        "Reduced motion and touch input keep the card flat"
      ]
    },
    "examples": [
      "tilt-card-demo",
      "tilt-card-product"
    ],
    "url": "https://ui.ballmac.com/components/tilt-card"
  },
  "categories": [
    "motion"
  ],
  "type": "registry:ui"
}