diff --git a/apps/web/src/arcade-games/know-your-world/components/MapRenderer.tsx b/apps/web/src/arcade-games/know-your-world/components/MapRenderer.tsx
index 79cef242..679c457b 100644
--- a/apps/web/src/arcade-games/know-your-world/components/MapRenderer.tsx
+++ b/apps/web/src/arcade-games/know-your-world/components/MapRenderer.tsx
@@ -7,7 +7,7 @@ import { useTheme } from '@/contexts/ThemeContext'
import { useVisualDebugSafe } from '@/contexts/VisualDebugContext'
import type { ContinentId } from '../continents'
import { usePulsingAnimation } from '../features/animations'
-import { CustomCursor } from '../features/cursor'
+import { CustomCursor, HeatCrosshair } from '../features/cursor'
import { useInteractionStateMachine } from '../features/interaction'
import { getRenderedViewport, LabelLayer, useD3ForceLabels } from '../features/labels'
import {
@@ -3391,65 +3391,12 @@ export function MapRenderer({
transform: 'translate(-50%, -50%)',
}}
>
- {/* Compass-style crosshair with heat effects - ring rotates, N stays fixed */}
- `rotate(${a}deg)`),
- }}
- >
- {/* Outer ring */}
-
- {/* Compass tick marks - 12 ticks around the ring */}
- {[0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330].map((angle) => {
- const isCardinal = angle % 90 === 0
- const rad = (angle * Math.PI) / 180
- const innerR = isCardinal ? 10 : 14
- const outerR = 16
- return (
-
- )
- })}
- {/* Center dot */}
-
- {/* Counter-rotating group to keep N fixed pointing up */}
- `rotate(${-a}deg)`),
- }}
- >
- {/* North indicator - red triangle pointing up */}
-
-
-
+
)}
diff --git a/apps/web/src/arcade-games/know-your-world/features/cursor/CustomCursor.tsx b/apps/web/src/arcade-games/know-your-world/features/cursor/CustomCursor.tsx
index ff93d687..f6f798b3 100644
--- a/apps/web/src/arcade-games/know-your-world/features/cursor/CustomCursor.tsx
+++ b/apps/web/src/arcade-games/know-your-world/features/cursor/CustomCursor.tsx
@@ -7,9 +7,10 @@
'use client'
-import { animated, type SpringValue } from '@react-spring/web'
+import type { SpringValue } from '@react-spring/web'
import { memo } from 'react'
import type { HeatCrosshairStyle } from '../../utils/heatStyles'
+import { HeatCrosshair } from './HeatCrosshair'
// ============================================================================
// Types
@@ -42,12 +43,6 @@ export interface CustomCursorProps {
flagEmoji?: string | null
}
-// ============================================================================
-// Constants
-// ============================================================================
-
-const COMPASS_ANGLES = [0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330]
-
// ============================================================================
// Component
// ============================================================================
@@ -99,59 +94,7 @@ export const CustomCursor = memo(function CustomCursor({
transition: 'transform 0.1s ease-out',
}}
>
- {/* Compass-style crosshair with heat effects - ring rotates, N stays fixed */}
- `rotate(${a}deg)`),
- }}
- >
- {/* Outer ring */}
-
- {/* Compass tick marks - 12 ticks around the ring */}
- {COMPASS_ANGLES.map((angle) => {
- const isCardinal = angle % 90 === 0
- const rad = (angle * Math.PI) / 180
- const innerR = isCardinal ? 9 : 11
- const outerR = 13
- return (
-
- )
- })}
- {/* Center dot */}
-
- {/* Counter-rotating group to keep N fixed pointing up */}
- `rotate(${-a}deg)`),
- }}
- >
- {/* North indicator - red triangle pointing up */}
-
-
-
+
{/* Cursor region name label - shows what to find under the cursor */}
diff --git a/apps/web/src/arcade-games/know-your-world/features/cursor/HeatCrosshair.tsx b/apps/web/src/arcade-games/know-your-world/features/cursor/HeatCrosshair.tsx
new file mode 100644
index 00000000..fb535446
--- /dev/null
+++ b/apps/web/src/arcade-games/know-your-world/features/cursor/HeatCrosshair.tsx
@@ -0,0 +1,144 @@
+/**
+ * Heat Crosshair Component
+ *
+ * Reusable compass-style crosshair SVG with heat-based styling.
+ * Features rotating outer ring with fixed north indicator.
+ */
+
+'use client'
+
+import { animated, type SpringValue } from '@react-spring/web'
+import { memo } from 'react'
+import type { HeatCrosshairStyle } from '../../utils/heatStyles'
+
+// ============================================================================
+// Constants
+// ============================================================================
+
+const COMPASS_ANGLES = [0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330]
+
+// ============================================================================
+// Types
+// ============================================================================
+
+export interface HeatCrosshairProps {
+ /** Size of the crosshair in pixels */
+ size: number
+ /** Rotation angle spring value (degrees) */
+ rotationAngle: SpringValue
+ /** Heat-based styling for crosshair */
+ heatStyle: HeatCrosshairStyle
+ /** Drop shadow intensity (0-1), default 0.5 */
+ shadowIntensity?: number
+}
+
+// ============================================================================
+// Component
+// ============================================================================
+
+/**
+ * Compass-style crosshair SVG with heat-based styling.
+ *
+ * Renders:
+ * - Outer ring that rotates based on heat level
+ * - 12 compass tick marks (cardinal directions highlighted in white)
+ * - Center dot
+ * - North indicator (red triangle) that counter-rotates to stay pointing up
+ *
+ * All dimensions are calculated proportionally based on the `size` prop.
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ */
+export const HeatCrosshair = memo(function HeatCrosshair({
+ size,
+ rotationAngle,
+ heatStyle,
+ shadowIntensity = 0.5,
+}: HeatCrosshairProps) {
+ // Calculate proportional dimensions based on size
+ const center = size / 2
+ const ringRadius = size * 0.40625 // 13/32 or 16/40
+ const cardinalInnerR = ringRadius * 0.69 // 9/13 or 10/16
+ const minorInnerR = ringRadius * 0.846 // 11/13 or 14/16
+ const centerDotRadius = size * 0.047 // 1.5/32 or ~1.9/40
+ const northTriangleSize = size * 0.125 // 4/32 or 5/40
+
+ // Cardinal tick stroke width scales with size
+ const cardinalStrokeWidth = size > 36 ? 2.5 : 2
+ const shadowBlur = size > 36 ? 3 : 2
+
+ return (
+ `rotate(${a}deg)`),
+ }}
+ >
+ {/* Outer ring */}
+
+
+ {/* Compass tick marks - 12 ticks around the ring */}
+ {COMPASS_ANGLES.map((angle) => {
+ const isCardinal = angle % 90 === 0
+ const rad = (angle * Math.PI) / 180
+ const innerR = isCardinal ? cardinalInnerR : minorInnerR
+ const outerR = ringRadius
+ return (
+
+ )
+ })}
+
+ {/* Center dot */}
+
+
+ {/* Counter-rotating group to keep N fixed pointing up */}
+ `rotate(${-a}deg)`),
+ }}
+ >
+ {/* North indicator - red triangle pointing up */}
+
+
+
+ )
+})
diff --git a/apps/web/src/arcade-games/know-your-world/features/cursor/index.ts b/apps/web/src/arcade-games/know-your-world/features/cursor/index.ts
index 852dbe08..f8f34cb7 100644
--- a/apps/web/src/arcade-games/know-your-world/features/cursor/index.ts
+++ b/apps/web/src/arcade-games/know-your-world/features/cursor/index.ts
@@ -10,3 +10,6 @@ export type {
CustomCursorProps,
} from './CustomCursor'
export { CustomCursor } from './CustomCursor'
+
+export type { HeatCrosshairProps } from './HeatCrosshair'
+export { HeatCrosshair } from './HeatCrosshair'