{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tilt-card",
  "type": "registry:ui",
  "title": "Tilt Card",
  "description": "A surface that leans away from the pointer in 3D and settles back on leave.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/loomui/tilt-card.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface TiltCardProps extends React.ComponentProps<\"div\"> {\n  /** Largest tilt in degrees, reached at the corners. */\n  max?: number\n  /** Depth of the perspective, as a CSS length. Lower is more extreme. */\n  perspective?: string\n  /** Scale held while the pointer is over the card. */\n  scale?: number\n  /** Lay a sheen over the card that follows the pointer. */\n  glare?: boolean\n  /** Render the card flat. */\n  disabled?: boolean\n}\n\nexport function TiltCard({\n  children,\n  className,\n  max = 10,\n  perspective = \"800px\",\n  scale = 1.02,\n  glare = true,\n  disabled = false,\n  style,\n  ...props\n}: TiltCardProps) {\n  const ref = React.useRef<HTMLDivElement>(null)\n  const frame = React.useRef(0)\n  const [reduced, setReduced] = React.useState(false)\n\n  React.useEffect(() => {\n    const query = window.matchMedia(\"(prefers-reduced-motion: reduce)\")\n    setReduced(query.matches)\n\n    const onChange = (event: MediaQueryListEvent) => setReduced(event.matches)\n    query.addEventListener(\"change\", onChange)\n    return () => query.removeEventListener(\"change\", onChange)\n  }, [])\n\n  // A tilt is the whole component, so there is nothing to soften for someone\n  // who asked for less motion. The card is rendered flat instead.\n  const flat = disabled || reduced\n\n  const write = React.useCallback((x: number, y: number, active: number) => {\n    const node = ref.current\n    if (!node) {\n      return\n    }\n    node.style.setProperty(\"--tilt-x\", `${x}`)\n    node.style.setProperty(\"--tilt-y\", `${y}`)\n    node.style.setProperty(\"--tilt-active\", `${active}`)\n  }, [])\n\n  const reset = React.useCallback(() => {\n    cancelAnimationFrame(frame.current)\n    write(50, 50, 0)\n  }, [write])\n\n  // Pointer position lands on custom properties inside a frame, so a move\n  // costs a compositor update rather than a React render.\n  const handlePointerMove = React.useCallback(\n    (event: React.PointerEvent<HTMLDivElement>) => {\n      if (flat) {\n        return\n      }\n      const { clientX, clientY } = event\n      cancelAnimationFrame(frame.current)\n      frame.current = requestAnimationFrame(() => {\n        const node = ref.current\n        if (!node) {\n          return\n        }\n        const rect = node.getBoundingClientRect()\n        write(\n          ((clientX - rect.left) / rect.width) * 100,\n          ((clientY - rect.top) / rect.height) * 100,\n          1\n        )\n      })\n    },\n    [flat, write]\n  )\n\n  React.useEffect(() => () => cancelAnimationFrame(frame.current), [])\n\n  return (\n    <div\n      data-slot=\"tilt-card\"\n      className={cn(\"group\", className)}\n      style={{ perspective, ...style }}\n      {...props}\n    >\n      <div\n        ref={ref}\n        onPointerMove={handlePointerMove}\n        onPointerLeave={reset}\n        onBlur={reset}\n        className={cn(\n          \"relative isolate size-full rounded-[inherit]\",\n          !flat &&\n            \"transition-transform duration-[240ms] ease-[var(--ease-out-quart)] will-change-transform\"\n        )}\n        style={\n          {\n            \"--tilt-max\": `${max}deg`,\n            // A pointer at the top of the card tilts its top away, so the sign\n            // on the X axis is inverted against the Y axis.\n            transform: flat\n              ? undefined\n              : [\n                  \"rotateX(calc((var(--tilt-y, 50) - 50) / -50 * var(--tilt-max) * var(--tilt-active, 0)))\",\n                  \"rotateY(calc((var(--tilt-x, 50) - 50) / 50 * var(--tilt-max) * var(--tilt-active, 0)))\",\n                  `scale(calc(1 + ${scale - 1} * var(--tilt-active, 0)))`,\n                ].join(\" \"),\n          } as React.CSSProperties\n        }\n      >\n        {glare && !flat ? (\n          <span\n            aria-hidden=\"true\"\n            className=\"pointer-events-none absolute inset-0 z-10 rounded-[inherit] opacity-[var(--tilt-active,0)] transition-opacity duration-[240ms]\"\n            style={{\n              background:\n                \"radial-gradient(60% 60% at calc(var(--tilt-x, 50) * 1%) calc(var(--tilt-y, 50) * 1%), color-mix(in oklch, #fff 26%, transparent), transparent)\",\n            }}\n          />\n        ) : null}\n        {children}\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "cssVars": {
    "theme": {
      "ease-out-quart": "cubic-bezier(0.165, 0.84, 0.44, 1)"
    }
  }
}