{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tilted-card",
  "title": "Tilted Card",
  "author": "AlgorizeTech",
  "description": "A card that tilts in 3D toward the pointer with a moving glare highlight. Use it to make a single feature or product card feel tactile.",
  "dependencies": [],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/custom/blocks/TiltedCardBlock.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport React, { useEffect, useRef } from \"react\";\nimport { createSpringFollower, type SpringFollower } from \"./motion-curves\";\n\nexport interface TiltedCardProps extends React.HTMLAttributes<HTMLDivElement> {\n  children: React.ReactNode;\n  tiltIntensity?: number; // Maximum rotation in degrees (e.g., 15)\n  glareOpacity?: number; // Maximum opacity of the glare effect (0 to 1)\n  scaleOnHover?: number; // Scale factor when hovered (e.g., 1.05)\n  initialScale?: number; // Starting scale factor (e.g., 1 or 1.05 for featured cards)\n  innerClassName?: string;\n  glareClassName?: string;\n  showShadow?: boolean;\n}\n\n// Pointer tilt: every frame the tilt chases the pointer through this spring,\n// keeping its velocity when the pointer moves on — no animation library, just\n// the spring's equation of motion stepped on requestAnimationFrame.\nconst TILT_SPRING = { damping: 25, stiffness: 300, mass: 0.5, restDelta: 0.001, restSpeed: 0.01 };\n// Hover scale, same approach, so leaving mid-grow springs back from where it is.\nconst HOVER_SPRING = { stiffness: 300, damping: 20, mass: 1, restDelta: 0.005, restSpeed: 0.01 };\n\n/** Maps -1..1 onto `from..to`, clamped at both ends. */\nconst mapRange = (v: number, from: number, to: number) => {\n  const p = Math.min(Math.max((v + 1) / 2, 0), 1);\n  return from + (to - from) * p;\n};\n\nconst glareBackground = (x: number, y: number) =>\n  `radial-gradient(circle at ${x}% ${y}%, var(--theme-color,var(--pricing-accent,var(--primary))), transparent 65%)`;\n\nexport const TiltedCard = React.forwardRef<HTMLDivElement, TiltedCardProps>(\n  (\n    {\n      children,\n      className,\n      tiltIntensity = 15,\n      glareOpacity = 0.2,\n      scaleOnHover = 1.02,\n      initialScale = 1,\n      innerClassName,\n      glareClassName,\n      showShadow = true,\n      ...props\n    },\n    ref,\n  ) => {\n    const cardRef = useRef<HTMLDivElement>(null);\n    const glareRef = useRef<HTMLDivElement>(null);\n\n    React.useImperativeHandle(ref, () => cardRef.current as HTMLDivElement);\n\n    // Read at paint time, so a changed prop applies from the next frame.\n    const latest = useRef({ tiltIntensity, scaleOnHover, initialScale });\n    latest.current = { tiltIntensity, scaleOnHover, initialScale };\n\n    const springs = useRef<{\n      x: SpringFollower;\n      y: SpringFollower;\n      scale: SpringFollower;\n    } | null>(null);\n\n    useEffect(() => {\n      // Writes the card transform and the glare straight to the DOM, the way\n      // motion values do — no React render per frame.\n      const paint = () => {\n        const s = springs.current;\n        const card = cardRef.current;\n        if (!s || !card) return;\n        const tilt = latest.current.tiltIntensity;\n        const x = s.x.get();\n        const y = s.y.get();\n        const scale = s.scale.get();\n        const rotateX = mapRange(y, tilt, -tilt);\n        const rotateY = mapRange(x, -tilt, tilt);\n        const parts: string[] = [];\n        if (scale !== 1) parts.push(`scale(${scale})`);\n        if (rotateX !== 0) parts.push(`rotateX(${rotateX}deg)`);\n        if (rotateY !== 0) parts.push(`rotateY(${rotateY}deg)`);\n        card.style.transform = parts.length ? parts.join(\" \") : \"none\";\n        if (glareRef.current) {\n          glareRef.current.style.background = glareBackground(\n            mapRange(x, 0, 100),\n            mapRange(y, 0, 100),\n          );\n        }\n      };\n\n      springs.current = {\n        x: createSpringFollower(0, paint, TILT_SPRING),\n        y: createSpringFollower(0, paint, TILT_SPRING),\n        scale: createSpringFollower(initialScale, paint, HOVER_SPRING),\n      };\n\n      paint();\n\n      return () => {\n        springs.current?.x.destroy();\n        springs.current?.y.destroy();\n        springs.current?.scale.destroy();\n        springs.current = null;\n      };\n    }, [initialScale]);\n\n    const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {\n      if (!cardRef.current) return;\n      const rect = cardRef.current.getBoundingClientRect();\n      const x = (e.clientX - rect.left) / rect.width - 0.5;\n      const y = (e.clientY - rect.top) / rect.height - 0.5;\n      springs.current?.x.set(x * 2);\n      springs.current?.y.set(y * 2);\n    };\n\n    const handleMouseLeave = () => {\n      springs.current?.x.set(0);\n      springs.current?.y.set(0);\n    };\n\n    // Hover scale follows a mouse or pen; a touch never counts as hovering.\n    const handlePointerEnter = (e: React.PointerEvent<HTMLDivElement>) => {\n      if (e.pointerType === \"touch\") return;\n      springs.current?.scale.set(latest.current.scaleOnHover);\n    };\n\n    const handlePointerLeave = (e: React.PointerEvent<HTMLDivElement>) => {\n      if (e.pointerType === \"touch\") return;\n      springs.current?.scale.set(latest.current.initialScale);\n    };\n\n    return (\n      <div\n        className={cn(\"relative perspective-[1000px] w-full h-full\", className)}\n        {...props}\n      >\n        <div\n          ref={cardRef}\n          onMouseMove={handleMouseMove}\n          onMouseLeave={handleMouseLeave}\n          onPointerEnter={handlePointerEnter}\n          onPointerLeave={handlePointerLeave}\n          style={{\n            transformStyle: \"preserve-3d\",\n            transform: initialScale !== 1 ? `scale(${initialScale})` : undefined,\n          }}\n          className={cn(\n            \"relative w-full h-full rounded-2xl overflow-visible transition-shadow duration-300 group cursor-pointer\",\n            showShadow &&\n              \"shadow-[0_10px_30px_rgba(0,0,0,0.08)] dark:shadow-[0_10px_30px_rgba(0,0,0,0.5)] hover:shadow-[0_20px_40px_rgba(0,0,0,0.14)] dark:hover:shadow-[0_20px_40px_rgba(0,0,0,0.7)]\",\n            innerClassName,\n          )}\n        >\n          <div\n            className=\"relative w-full h-full z-10\"\n            style={\n              {\n                transform: \"translateZ(30px)\",\n                \"--glare-opacity\": glareOpacity,\n              } as React.CSSProperties\n            }\n          >\n            {children}\n\n            {/* Dynamic Theme Color Glare Overlay */}\n            <div\n              ref={glareRef}\n              className={cn(\n                \"absolute inset-0 z-50 pointer-events-none opacity-0 group-hover:opacity-[var(--glare-opacity)] transition-opacity duration-300 rounded-2xl\",\n                glareClassName,\n              )}\n              style={{ background: glareBackground(50, 50) }}\n            />\n          </div>\n        </div>\n      </div>\n    );\n  },\n);\nTiltedCard.displayName = \"TiltedCard\";\n",
      "type": "registry:component",
      "target": "components/custom/blocks/TiltedCardBlock.tsx"
    },
    {
      "path": "src/components/custom/blocks/motion-curves.ts",
      "content": "/**\n * Motion curves with no animation library behind them.\n *\n * The components here ship as CSS, GSAP or Motion builds that must move\n * identically, so the curves are computed once, from the same numbers, and\n * handed to whichever engine runs them:\n *\n *   - `cubicBezier(x1, y1, x2, y2)` → an easing function (GSAP accepts any\n *     `(progress) => value` as `ease`), exact for Motion's `ease: [..]` arrays\n *     and for CSS `cubic-bezier()`.\n *   - `spring({ stiffness, damping, mass })` → the analytic solution Motion\n *     Motion uses, as an easing function plus the time it takes to settle, or\n *     as a CSS `linear()` string for a fixed start and end.\n *   - `createSpringFollower` → a value that chases a moving target through the\n *     same spring every frame, like Motion's `useSpring`, for pointer-driven\n *     motion that no fixed curve can describe.\n */\n\n// ---------------------------------------------------------------------------\n// Cubic bezier\n// ---------------------------------------------------------------------------\n\n/**\n * The CSS / Motion cubic-bezier as a function of progress. Newton-Raphson\n * with a bisection fallback, the same solver browsers use.\n */\nexport function cubicBezier(x1: number, y1: number, x2: number, y2: number) {\n  if (x1 === y1 && x2 === y2) return (t: number) => t;\n\n  const a = (p1: number, p2: number) => 1 - 3 * p2 + 3 * p1;\n  const b = (p1: number, p2: number) => 3 * p2 - 6 * p1;\n  const c = (p1: number) => 3 * p1;\n  const at = (t: number, p1: number, p2: number) =>\n    ((a(p1, p2) * t + b(p1, p2)) * t + c(p1)) * t;\n  const slope = (t: number, p1: number, p2: number) =>\n    3 * a(p1, p2) * t * t + 2 * b(p1, p2) * t + c(p1);\n\n  const solveX = (x: number) => {\n    let t = x;\n    for (let i = 0; i < 8; i++) {\n      const s = slope(t, x1, x2);\n      if (Math.abs(s) < 1e-6) break;\n      const err = at(t, x1, x2) - x;\n      if (Math.abs(err) < 1e-7) return t;\n      t -= err / s;\n    }\n    let lo = 0;\n    let hi = 1;\n    t = x;\n    for (let i = 0; i < 30 && hi - lo > 1e-7; i++) {\n      const value = at(t, x1, x2);\n      if (value < x) lo = t;\n      else hi = t;\n      t = (lo + hi) / 2;\n    }\n    return t;\n  };\n\n  return (x: number) => (x <= 0 ? 0 : x >= 1 ? 1 : at(solveX(x), y1, y2));\n}\n\n/** Motion's named eases, as the cubic-beziers they are. */\nexport const MOTION_EASE = {\n  linear: [0, 0, 1, 1],\n  easeIn: [0.42, 0, 1, 1],\n  easeOut: [0, 0, 0.58, 1],\n  easeInOut: [0.42, 0, 0.58, 1],\n  circIn: [0.55, 0, 1, 0.45],\n  circOut: [0.55, 0, 0.45, 1],\n  circInOut: [0.85, 0, 0.15, 1],\n  backIn: [0.31, 0.01, 0.66, -0.59],\n  backOut: [0.33, 1.53, 0.69, 0.99],\n  anticipate: [0.66, -0.56, 0.34, 1.35],\n} as const;\n\n/**\n * Motion's default layout transition (`layout` / `layoutId`):\n * `{ duration: 0.45, ease: [0.4, 0, 0.1, 1] }`.\n */\nexport const LAYOUT_TRANSITION = { duration: 0.45, ease: [0.4, 0, 0.1, 1] as const };\n\n// ---------------------------------------------------------------------------\n// Spring\n// ---------------------------------------------------------------------------\n\nexport interface SpringOptions {\n  /** Motion default 100. */\n  stiffness?: number;\n  /** Motion default 10. */\n  damping?: number;\n  /** Motion default 1. */\n  mass?: number;\n  /** Starting velocity, in units of the whole distance per second. */\n  velocity?: number;\n  /** How close to the end counts as settled, as a fraction of the distance. */\n  restDelta?: number;\n}\n\n/**\n * Normalised position (0 → 1) of a spring released from 0 towards 1, at time\n * `t` seconds. The closed-form solution Motion's `spring()` generator\n * uses for the under-, critically and over-damped cases.\n */\nexport function springAt(options: SpringOptions = {}) {\n  const { stiffness = 100, damping = 10, mass = 1, velocity = 0 } = options;\n  const zeta = damping / (2 * Math.sqrt(stiffness * mass));\n  const omega0 = Math.sqrt(stiffness / mass);\n  // Motion's convention: distance still to cover, velocity toward it negated.\n  const delta = 1;\n  const v0 = -velocity;\n\n  if (zeta < 1) {\n    const omegaD = omega0 * Math.sqrt(1 - zeta * zeta);\n    return (t: number) => {\n      const envelope = Math.exp(-zeta * omega0 * t);\n      return (\n        1 -\n        envelope *\n          (((v0 + zeta * omega0 * delta) / omegaD) * Math.sin(omegaD * t) +\n            delta * Math.cos(omegaD * t))\n      );\n    };\n  }\n  if (zeta === 1) {\n    return (t: number) => 1 - Math.exp(-omega0 * t) * (delta + (v0 + omega0 * delta) * t);\n  }\n  const omegaD = omega0 * Math.sqrt(zeta * zeta - 1);\n  return (t: number) => {\n    const envelope = Math.exp(-zeta * omega0 * t);\n    const freq = Math.min(omegaD * t, 300);\n    return (\n      1 -\n      (envelope *\n        ((v0 + zeta * omega0 * delta) * Math.sinh(freq) + omegaD * delta * Math.cosh(freq))) /\n        omegaD\n    );\n  };\n}\n\n/** Seconds until the spring stays within `restDelta` of its end. */\nexport function springDuration(options: SpringOptions = {}) {\n  const at = springAt(options);\n  const restDelta = options.restDelta ?? 0.001;\n  const step = 1 / 240;\n  let lastOutside = 0;\n  for (let t = 0; t <= 10; t += step) {\n    if (Math.abs(1 - at(t)) > restDelta) lastOutside = t;\n    else if (t - lastOutside > 0.1) break;\n  }\n  return Math.max(lastOutside + step, step);\n}\n\n/**\n * A spring as a duration and an easing function over it — what GSAP takes:\n *\n *   const { duration, ease } = springEase({ stiffness: 300, damping: 20 });\n *   gsap.to(el, { y: 0, duration, ease });\n *\n * Exact for a fixed start and end; a spring that has to react mid-flight to a\n * new target needs `createSpringFollower` instead.\n */\nexport function springEase(options: SpringOptions = {}) {\n  const duration = springDuration(options);\n  const at = springAt(options);\n  return {\n    duration,\n    ease: (progress: number) => (progress >= 1 ? 1 : at(progress * duration)),\n  };\n}\n\n/**\n * A spring as a CSS `linear()` easing and its duration in ms, for\n * `transition` / `animation` with a fixed start and end.\n *\n *   const { easing, duration } = springLinear({ stiffness: 400, damping: 30 });\n *   el.style.transition = `transform ${duration}ms ${easing}`;\n */\nexport function springLinear(options: SpringOptions = {}, pointsPerSecond = 60) {\n  const duration = springDuration(options);\n  const at = springAt(options);\n  const count = Math.max(Math.ceil(duration * pointsPerSecond), 8);\n  const points: string[] = [];\n  for (let i = 0; i <= count; i++) {\n    const t = (i / count) * duration;\n    points.push(String(Math.round((i === count ? 1 : at(t)) * 10000) / 10000));\n  }\n  return { easing: `linear(${points.join(\", \")})`, duration: Math.round(duration * 1000) };\n}\n\n// ---------------------------------------------------------------------------\n// Spring follower (useSpring equivalent)\n// ---------------------------------------------------------------------------\n\n/** Something that calls back once per frame with the time since the last, in ms. */\nexport interface FrameTicker {\n  add: (callback: (deltaMs: number) => void) => () => void;\n}\n\n/** requestAnimationFrame as a ticker — the library-free default. */\nexport const rafTicker: FrameTicker = {\n  add(callback) {\n    let last = performance.now();\n    let frame = requestAnimationFrame(function tick(now) {\n      callback(now - last);\n      last = now;\n      frame = requestAnimationFrame(tick);\n    });\n    return () => cancelAnimationFrame(frame);\n  },\n};\n\nexport interface SpringFollower {\n  /** Chase a new target from wherever the value is now, keeping its velocity. */\n  set: (target: number) => void;\n  /** Move to a value instantly, with no motion. */\n  jump: (value: number) => void;\n  get: () => number;\n  destroy: () => void;\n}\n\n/**\n * A value that follows its target through a spring, integrating the same\n * equation of motion Motion's `useSpring` solves — so a pointer-driven spring\n * reads the same in every build. Runs only while moving; `onUpdate` receives\n * each new value.\n */\nexport function createSpringFollower(\n  initial: number,\n  onUpdate: (value: number) => void,\n  options: SpringOptions & { restSpeed?: number; ticker?: FrameTicker } = {},\n): SpringFollower {\n  const { stiffness = 100, damping = 10, mass = 1, ticker = rafTicker } = options;\n  const restDelta = options.restDelta ?? 0.01;\n  const restSpeed = options.restSpeed ?? 0.01;\n  let value = initial;\n  let target = initial;\n  let velocity = 0;\n  let stop: (() => void) | null = null;\n\n  const step = (deltaMs: number) => {\n    // Fixed 1ms sub-steps keep the integration stable for stiff springs and\n    // independent of the frame rate.\n    const steps = Math.min(Math.max(Math.round(deltaMs), 1), 250);\n    for (let i = 0; i < steps; i++) {\n      const force = -stiffness * (value - target) - damping * velocity;\n      velocity += (force / mass) * 0.001;\n      value += velocity * 0.001;\n    }\n    if (Math.abs(target - value) < restDelta && Math.abs(velocity) < restSpeed) {\n      value = target;\n      velocity = 0;\n      stop?.();\n      stop = null;\n    }\n    onUpdate(value);\n  };\n\n  return {\n    set(next) {\n      target = next;\n      if (!stop && value !== target) stop = ticker.add(step);\n    },\n    jump(next) {\n      stop?.();\n      stop = null;\n      value = target = next;\n      velocity = 0;\n      onUpdate(value);\n    },\n    get: () => value,\n    destroy() {\n      stop?.();\n      stop = null;\n    },\n  };\n}\n",
      "type": "registry:component",
      "target": "components/custom/blocks/motion-curves.ts"
    }
  ],
  "docs": "Named export: TiltedCard. Wraps arbitrary children; tune `tiltIntensity`, `glareOpacity` and `scaleOnHover`. Pointer-driven, so it is inert on touch — keep the content readable without the effect.",
  "categories": [
    "media",
    "marketing"
  ],
  "type": "registry:component"
}
