All files / web/src/arcade-games/know-your-world/features/magnifier MagnifierContext.tsx

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

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 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               
/**
 * Magnifier Context
 *
 * Provides magnifier state and utilities to child components,
 * avoiding deep prop drilling while maintaining type safety.
 *
 * This context consolidates:
 * - Magnifier visibility and expansion state
 * - Zoom state and animation
 * - Touch interaction state
 * - Cursor position and viewport calculations
 * - Interaction state machine
 * - Springs and animations
 */

'use client'

import type { SpringValue } from '@react-spring/web'
import { createContext, type ReactNode, type RefObject, useContext, useMemo } from 'react'

import type { UseInteractionStateMachineReturn } from '../interaction'

// ============================================================================
// Types
// ============================================================================

export interface MagnifierSpring {
  top: SpringValue<number>
  left: SpringValue<number>
  width: SpringValue<number>
  height: SpringValue<number>
  opacity: SpringValue<number>
  movementMultiplier: SpringValue<number>
}

export interface ParsedViewBox {
  x: number
  y: number
  width: number
  height: number
}

export interface SafeZoneMargins {
  top: number
  right: number
  bottom: number
  left: number
}

export interface PrecisionCalculations {
  isAtThreshold: boolean
  screenPixelRatio: number
}

// ============================================================================
// Context Value Type
// ============================================================================

export interface MagnifierContextValue {
  // -------------------------------------------------------------------------
  // Refs
  // -------------------------------------------------------------------------
  /** Container element ref */
  containerRef: RefObject<HTMLDivElement>
  /** SVG element ref */
  svgRef: RefObject<SVGSVGElement>
  /** Magnifier element ref */
  magnifierRef: RefObject<HTMLDivElement>
  /** Cursor position ref (mutable) */
  cursorPositionRef: React.MutableRefObject<{ x: number; y: number } | null>
  /** Scale probe 1 ref (for empirical scale measurement) */
  scaleProbe1Ref: RefObject<SVGCircleElement>
  /** Scale probe 2 ref (for empirical scale measurement) */
  scaleProbe2Ref: RefObject<SVGCircleElement>
  /** Anchor probe ref (for closed-loop 1:1 tracking) */
  anchorProbeRef: RefObject<SVGCircleElement>
  /** Anchor SVG position - set on touch start, probe stays at this SVG coord */
  anchorSvgPositionRef: React.MutableRefObject<{ x: number; y: number } | null>

  // -------------------------------------------------------------------------
  // Position & Animation
  // -------------------------------------------------------------------------
  /** Current cursor position in container coordinates (from state machine) */
  cursorPosition: { x: number; y: number } | null
  // Note: setCursorPosition removed - dispatch to interaction state machine instead
  /** Zoom spring value */
  zoomSpring: SpringValue<number>
  /** Magnifier position/opacity springs */
  magnifierSpring: MagnifierSpring
  /** Parsed viewBox dimensions */
  parsedViewBox: ParsedViewBox
  /** Safe zone margins for UI elements */
  safeZoneMargins: SafeZoneMargins

  // -------------------------------------------------------------------------
  // Magnifier State
  // -------------------------------------------------------------------------
  /** Whether magnifier is visible */
  showMagnifier: boolean
  /** Set magnifier visibility */
  setShowMagnifier: (show: boolean) => void
  /** Whether magnifier is expanded (full size) */
  isMagnifierExpanded: boolean
  /** Set magnifier expansion */
  setIsMagnifierExpanded: (expanded: boolean) => void
  /** Target opacity for magnifier */
  targetOpacity: number
  /** Set target opacity */
  setTargetOpacity: (opacity: number) => void
  /** Target zoom level */
  targetZoom: number
  /** Set target zoom */
  setTargetZoom: (zoom: number) => void

  // -------------------------------------------------------------------------
  // Interaction State Machine
  // -------------------------------------------------------------------------
  /** Full interaction state machine return */
  interaction: UseInteractionStateMachineReturn

  // -------------------------------------------------------------------------
  // Derived Interaction State (convenience)
  // -------------------------------------------------------------------------
  /** Whether mobile map dragging triggered the magnifier */
  mobileMapDragTriggeredMagnifier: boolean
  /** Whether mobile map is being dragged */
  isMobileMapDragging: boolean
  /** Whether magnifier is being dragged */
  isMagnifierDragging: boolean
  /** Whether pointer is locked (precision mode) */
  pointerLocked: boolean

  // -------------------------------------------------------------------------
  // Device & Theme
  // -------------------------------------------------------------------------
  /** Whether dark mode is active */
  isDark: boolean
  /** Whether device is touch-based */
  isTouchDevice: boolean
  /** Whether device supports precision mode (pointer lock) */
  canUsePrecisionMode: boolean

  // -------------------------------------------------------------------------
  // Precision Mode
  // -------------------------------------------------------------------------
  /** Precision mode threshold */
  precisionModeThreshold: number
  /** Precision calculations */
  precisionCalcs: PrecisionCalculations

  // -------------------------------------------------------------------------
  // Zoom Controls
  // -------------------------------------------------------------------------
  /** Get current zoom level (from spring) */
  getCurrentZoom: () => number
  /** High zoom threshold for styling changes */
  highZoomThreshold: number
}

// ============================================================================
// Context Creation
// ============================================================================

const MagnifierContext = createContext<MagnifierContextValue | null>(null)

// ============================================================================
// Provider Component
// ============================================================================

export interface MagnifierProviderProps {
  children: ReactNode
  value: MagnifierContextValue
}

/**
 * Provider for Magnifier context.
 *
 * This provider should wrap the magnifier overlay and related components
 * to provide shared state without prop drilling.
 *
 * @example
 * ```tsx
 * <MagnifierProvider value={magnifierContextValue}>
 *   <MagnifierOverlay />
 *   <ZoomLinesOverlay />
 * </MagnifierProvider>
 * ```
 */
export function MagnifierProvider({ children, value }: MagnifierProviderProps) {
  // Memoize the value to prevent unnecessary re-renders
  // Only re-create when key values change
  const memoizedValue = useMemo(
    () => value,
    [
      // Refs (stable)
      value.containerRef,
      value.svgRef,
      value.magnifierRef,
      value.cursorPositionRef,
      value.scaleProbe1Ref,
      value.scaleProbe2Ref,
      value.anchorProbeRef,
      value.anchorSvgPositionRef,
      // Position & Animation
      value.cursorPosition,
      value.zoomSpring,
      value.magnifierSpring,
      value.parsedViewBox,
      value.safeZoneMargins,
      // Magnifier State
      value.showMagnifier,
      value.setShowMagnifier,
      value.isMagnifierExpanded,
      value.setIsMagnifierExpanded,
      value.targetOpacity,
      value.setTargetOpacity,
      value.targetZoom,
      value.setTargetZoom,
      // Interaction State Machine
      value.interaction,
      // Derived State
      value.mobileMapDragTriggeredMagnifier,
      value.isMobileMapDragging,
      value.isMagnifierDragging,
      value.pointerLocked,
      // Device & Theme
      value.isDark,
      value.isTouchDevice,
      value.canUsePrecisionMode,
      // Precision Mode
      value.precisionModeThreshold,
      value.precisionCalcs,
      // Zoom
      value.getCurrentZoom,
      value.highZoomThreshold,
    ]
  )

  return <MagnifierContext.Provider value={memoizedValue}>{children}</MagnifierContext.Provider>
}

// ============================================================================
// Hooks
// ============================================================================

/**
 * Access Magnifier context.
 *
 * @throws Error if used outside of MagnifierProvider
 * @returns Magnifier context value
 *
 * @example
 * ```tsx
 * function MagnifierControls() {
 *   const { showMagnifier, isMagnifierExpanded, isDark } = useMagnifierContext()
 *   // ...
 * }
 * ```
 */
export function useMagnifierContext(): MagnifierContextValue {
  const context = useContext(MagnifierContext)

  if (!context) {
    throw new Error('useMagnifierContext must be used within a MagnifierProvider')
  }

  return context
}

/**
 * Safely access Magnifier context (returns null if not available).
 *
 * Useful for components that can optionally use the context.
 *
 * @returns Magnifier context value or null
 */
export function useMagnifierContextSafe(): MagnifierContextValue | null {
  return useContext(MagnifierContext)
}