All files / web/src/hooks useDeviceTilt.ts

0% Statements 0/108
0% Branches 0/1
0% Functions 0/1
0% Lines 0/108

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109                                                                                                                                                                                                                         
'use client'

import { useCallback, useEffect, useRef, useState } from 'react'

export interface TiltVector {
  x: number
  y: number
}

/**
 * Hook that provides device tilt data via a ref (no re-renders).
 * Uses DeviceOrientationEvent to detect phone tilt and converts
 * it to a force vector suitable for physics simulations.
 *
 * On iOS 13+, permission must be requested via a user gesture (handled by `toggle`).
 */
export function useDeviceTilt() {
  const tiltRef = useRef<TiltVector>({ x: 0, y: 0 })
  const [supported, setSupported] = useState(false)
  const [enabled, setEnabled] = useState(false)
  const [needsPermission, setNeedsPermission] = useState(false)

  useEffect(() => {
    if (typeof window === 'undefined') return
    if ('DeviceOrientationEvent' in window) {
      setSupported(true)
      const DOE = DeviceOrientationEvent as unknown as {
        requestPermission?: () => Promise<string>
      }
      if (typeof DOE.requestPermission === 'function') {
        setNeedsPermission(true)
      }
    }
  }, [])

  const toggle = useCallback(async () => {
    if (enabled) {
      setEnabled(false)
      tiltRef.current = { x: 0, y: 0 }
      return
    }

    const DOE = DeviceOrientationEvent as unknown as {
      requestPermission?: () => Promise<string>
    }
    if (typeof DOE.requestPermission === 'function') {
      try {
        const result = await DOE.requestPermission()
        if (result !== 'granted') return
      } catch {
        return
      }
    }

    // Also request DeviceMotionEvent permission (needed for jolt detection).
    // On iOS this shares the same underlying permission dialog, so it should
    // succeed immediately if orientation was granted.
    const DME = DeviceMotionEvent as unknown as {
      requestPermission?: () => Promise<string>
    }
    if (typeof DME?.requestPermission === 'function') {
      try {
        await DME.requestPermission()
      } catch {
        // Non-fatal — jolt detection just won't work
      }
    }

    setEnabled(true)
    setNeedsPermission(false)
  }, [enabled])

  useEffect(() => {
    if (!enabled) return

    const SENSITIVITY = 0.12
    const MAX_TILT = 50
    const DEAD_ZONE = 2 // degrees - ignore tiny tilts
    const SMOOTHING = 0.3 // low-pass filter factor (0-1, lower = smoother)

    const clamp = (v: number, lo: number, hi: number) => Math.max(lo, Math.min(hi, v))

    const handler = (e: DeviceOrientationEvent) => {
      const rawBeta = e.beta ?? 0 // front-back: -180..180, 0 = flat
      const rawGamma = e.gamma ?? 0 // left-right: -90..90, 0 = flat

      // Apply dead zone
      const beta = Math.abs(rawBeta) < DEAD_ZONE ? 0 : rawBeta
      const gamma = Math.abs(rawGamma) < DEAD_ZONE ? 0 : rawGamma

      // Convert to force: gamma > 0 = tilted right = force right (positive x)
      //                   beta > 0 = tilted forward = force down (positive y)
      const targetX = clamp(gamma, -MAX_TILT, MAX_TILT) * SENSITIVITY
      const targetY = clamp(beta, -MAX_TILT, MAX_TILT) * SENSITIVITY

      // Low-pass filter for smooth movement
      tiltRef.current = {
        x: tiltRef.current.x + (targetX - tiltRef.current.x) * SMOOTHING,
        y: tiltRef.current.y + (targetY - tiltRef.current.y) * SMOOTHING,
      }
    }

    window.addEventListener('deviceorientation', handler)
    return () => window.removeEventListener('deviceorientation', handler)
  }, [enabled])

  return { tiltRef, supported, enabled, needsPermission, toggle }
}