{"version":3,"file":"tilt.mjs","names":[],"sources":["../../../../package/common/tilt/tilt.tsx"],"sourcesContent":["import { useEffect, useImperativeHandle, useRef, type PointerEvent as ReactPointerEvent } from 'react';\nimport cx from 'classnames';\n\nimport { useComponentDefaults } from '../configProvider';\nimport { useTiltGroup } from '../tiltGroup';\nimport { computeTilt, defaultTiltEasing } from './tiltMath';\nimport type { TiltProps } from './tilt.types';\nimport styles from './tilt.module.pcss';\nimport { isDev } from '../utils/isDev';\n\nfunction prefersReducedMotion(): boolean {\n  return typeof window !== 'undefined' && window.matchMedia?.('(prefers-reduced-motion: reduce)').matches === true;\n}\n\n// Per-frame catch-up rate (fraction of the remaining gap closed each tick) and the gap size,\n// in degrees, below which we consider the catch-up finished and switch to direct 1:1 writes.\nconst TILT_SMOOTHING = 0.3;\nconst TILT_CONVERGE_EPSILON = 0.05;\n\nfunction Tilt(props: TiltProps) {\n  const {\n    ref,\n    children,\n    maxTilt = 15,\n    maxTiltX,\n    maxTiltY,\n    perspective = 800,\n    easing = defaultTiltEasing,\n    lockAxis = 'none',\n    scaleOnHover = 1,\n    trackingMode = 'self',\n    disabled = false,\n    onTiltChange,\n    className,\n    style,\n    classNames,\n    styles: slotStyles,\n    ...rest\n  } = useComponentDefaults('Tilt', props);\n\n  const rootRef = useRef<HTMLDivElement>(null);\n  const surfaceRef = useRef<HTMLDivElement>(null);\n  const rafRef = useRef<number | undefined>(undefined);\n  const loopActiveRef = useRef(false);\n  // Whether the pointer is currently \"over\" the element (self mode: within its own bounds;\n  // group mode: the shared group pointer is set). The tilt loop's target is (0,0) whenever this\n  // is false, and computed from `latestPointerRef` whenever it's true.\n  const isHoveringRef = useRef(false);\n  // True while the visually-applied tilt is still catching up to the target after a\n  // discontinuity (entering from flat, or leaving back to flat), eased via a per-frame lerp\n  // toward whatever the *current* target is. Once the gap closes, updates apply directly (1:1,\n  // no lag) so continuous tracking stays crisp. A CSS transition can't do this cleanly: retargeting\n  // one mid-flight restarts its full duration from scratch, so a continuously-moving cursor makes\n  // it fall further and further behind instead of ever catching up.\n  const isCatchingUpRef = useRef(false);\n  const latestPointerRef = useRef<{ x: number; y: number } | null>(null);\n  const currentTiltRef = useRef({ rotateX: 0, rotateY: 0 });\n\n  const group = useTiltGroup();\n  const usingGroup = trackingMode === 'group';\n  const groupActive = usingGroup && group !== null;\n\n  useEffect(() => {\n    if (usingGroup && group === null && isDev()) {\n      console.warn('Tilt: trackingMode=\"group\" was set with no ancestor TiltGroup, falling back to self tracking.');\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [usingGroup, group === null]);\n\n  const clampX = maxTiltX ?? maxTilt;\n  const clampY = maxTiltY ?? maxTilt;\n\n  function writeTiltTransform(rotateX: number, rotateY: number, scale: number) {\n    const surface = surfaceRef.current;\n    if (!surface) return;\n    surface.style.transform = `rotateX(${rotateX}deg) rotateY(${rotateY}deg)` + (scale !== 1 ? ` scale(${scale})` : '');\n  }\n\n  function stopTiltLoop() {\n    if (rafRef.current !== undefined) {\n      cancelAnimationFrame(rafRef.current);\n      rafRef.current = undefined;\n    }\n    loopActiveRef.current = false;\n  }\n\n  // The single continuous per-frame driver for tilt. Reads live authoritative state\n  // (`isHoveringRef`/`latestPointerRef`) fresh every tick rather than closing over values\n  // captured at schedule time, so there's no possibility of a stale queued frame overwriting a\n  // newer state.\n  function tick() {\n    rafRef.current = undefined;\n    const surface = surfaceRef.current;\n    if (!surface) {\n      loopActiveRef.current = false;\n      return;\n    }\n\n    const hovering = isHoveringRef.current && !disabled && !prefersReducedMotion();\n    let targetX = 0;\n    let targetY = 0;\n    if (hovering) {\n      const pos = latestPointerRef.current;\n      const rect = rootRef.current?.getBoundingClientRect();\n      if (pos && rect) {\n        const target = computeTilt(pos.x - rect.left, pos.y - rect.top, rect, {\n          maxTiltX: clampX,\n          maxTiltY: clampY,\n          easing,\n          lockAxis,\n        });\n        targetX = target.rotateX;\n        targetY = target.rotateY;\n      }\n    }\n\n    const cur = currentTiltRef.current;\n    let nextX: number;\n    let nextY: number;\n    if (isCatchingUpRef.current) {\n      nextX = cur.rotateX + (targetX - cur.rotateX) * TILT_SMOOTHING;\n      nextY = cur.rotateY + (targetY - cur.rotateY) * TILT_SMOOTHING;\n      if (Math.abs(targetX - nextX) < TILT_CONVERGE_EPSILON && Math.abs(targetY - nextY) < TILT_CONVERGE_EPSILON) {\n        nextX = targetX;\n        nextY = targetY;\n        isCatchingUpRef.current = false;\n      }\n    } else {\n      // Locked on: apply the target directly, every frame. This is what keeps continuous\n      // tracking crisp with zero added lag once the catch-up phase has finished.\n      nextX = targetX;\n      nextY = targetY;\n    }\n\n    currentTiltRef.current = { rotateX: nextX, rotateY: nextY };\n    writeTiltTransform(nextX, nextY, hovering ? scaleOnHover : 1);\n    onTiltChange?.(nextX, nextY);\n\n    // Once locked on (nothing left to ease), stop rescheduling. The next native pointermove\n    // (self mode) or group pointer update (group mode) calls `ensureTiltLoopRunning()` again on\n    // its own, restarting the loop for exactly the one instant tick it needs.\n    if (!isCatchingUpRef.current) {\n      loopActiveRef.current = false;\n      return;\n    }\n    rafRef.current = requestAnimationFrame(tick);\n  }\n\n  function ensureTiltLoopRunning() {\n    if (loopActiveRef.current) return;\n    loopActiveRef.current = true;\n    rafRef.current = requestAnimationFrame(tick);\n  }\n\n  // Marks a hover-state transition (entering or leaving) so the loop re-arms its catch-up phase.\n  // Only on an actual flip, not on every pointermove while already hovering, so continuous\n  // tracking never re-triggers easing.\n  function setHovering(next: boolean) {\n    if (isHoveringRef.current !== next) {\n      isCatchingUpRef.current = true;\n    }\n    isHoveringRef.current = next;\n    ensureTiltLoopRunning();\n  }\n\n  // Self-mode pointer handlers.\n  const handlePointerMove = (event: ReactPointerEvent<HTMLDivElement>) => {\n    if (disabled || groupActive || prefersReducedMotion()) return;\n    latestPointerRef.current = { x: event.clientX, y: event.clientY };\n    setHovering(true);\n  };\n\n  const handlePointerLeave = () => {\n    if (groupActive) return;\n    setHovering(false);\n  };\n\n  // Group-mode tracking: react to the shared pointer position published by TiltGroup.\n  useEffect(() => {\n    if (!groupActive) return;\n    if (disabled || prefersReducedMotion()) {\n      setHovering(false);\n      return;\n    }\n    if (group?.pointer && group.rect) {\n      latestPointerRef.current = { x: group.rect.left + group.pointer.x, y: group.rect.top + group.pointer.y };\n      setHovering(true);\n    } else {\n      setHovering(false);\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [groupActive, disabled, group?.pointer, group?.rect]);\n\n  useEffect(() => {\n    // Must go through stopTiltLoop() (not just cancelAnimationFrame) so loopActiveRef is reset\n    // too. Under React StrictMode, every effect's cleanup runs once synchronously right after\n    // its setup, before setup runs again, so this fires even though the component isn't really\n    // unmounting. The group-mode effect above runs on every mount (it schedules the loop as soon\n    // as there's a group context, even with a still-null pointer), so cancelling its pending\n    // frame here without also clearing loopActiveRef left the loop believing it was still\n    // \"active\" with nothing actually scheduled: permanently wedged, since every later call to\n    // ensureTiltLoopRunning() saw loopActiveRef already true and silently no-opped forever.\n    return () => {\n      stopTiltLoop();\n    };\n  }, []);\n\n  useImperativeHandle(ref, () => ({\n    setTilt(rotateX: number, rotateY: number) {\n      const clamped = {\n        rotateX: Math.max(-clampX, Math.min(clampX, rotateX)),\n        rotateY: Math.max(-clampY, Math.min(clampY, rotateY)),\n      };\n      // Stops the loop so live tracking (if any is active) can't overwrite this explicit value\n      // on the very next frame. An external caller driving custom choreography expects the\n      // value it just set to actually stick. A later real pointer move restarts tracking fresh.\n      stopTiltLoop();\n      isHoveringRef.current = false;\n      isCatchingUpRef.current = false;\n      currentTiltRef.current = clamped;\n      writeTiltTransform(clamped.rotateX, clamped.rotateY, scaleOnHover);\n      onTiltChange?.(clamped.rotateX, clamped.rotateY);\n    },\n    reset() {\n      stopTiltLoop();\n      isHoveringRef.current = false;\n      isCatchingUpRef.current = false;\n      currentTiltRef.current = { rotateX: 0, rotateY: 0 };\n      writeTiltTransform(0, 0, 1);\n    },\n  }));\n\n  return (\n    <div\n      {...rest}\n      ref={rootRef}\n      className={cx(styles.tilt, className, classNames?.root)}\n      style={{ ...style, ...slotStyles?.root }}\n      onPointerMove={handlePointerMove}\n      onPointerLeave={handlePointerLeave}\n      onPointerCancel={handlePointerLeave}\n    >\n      <div\n        className={cx(styles.tiltPerspective, classNames?.perspective)}\n        style={{ perspective: `${perspective}px`, ...slotStyles?.perspective }}\n      >\n        <div ref={surfaceRef} className={cx(styles.tiltSurface, classNames?.surface)} style={slotStyles?.surface}>\n          {children}\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport default Tilt;\n"],"mappings":"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"}