import React, { useCallback, useEffect, useRef } from "react"; import { Animated, TouchableOpacity, View, StyleSheet, Easing, } from "react-native"; import { ExpandablePopover, gameUIColors } from "@react-buoy/shared-ui"; import { useMinimizedTools, MinimizedTool } from "./MinimizedToolsContext"; // ============================================================================ // Constants // ============================================================================ const PEEK_HEIGHT = 28; const TOOLBAR_WIDTH = 44; const TOOL_ITEM_SIZE = 32; const ICON_GAP = 6; const TOOLBAR_PADDING = 8; const COLLAPSE_BUTTON_SIZE = 24; const STORAGE_KEY_EXPANDED = "@react_buoy_minimized_stack_expanded"; // Glitch effect constants const GLITCH_DURATION_MS = 80; const MIN_GLITCH_DELAY = 2000; const MAX_GLITCH_DELAY = 6000; // ============================================================================ // Glitch Tool Button Component // ============================================================================ interface GlitchToolButtonProps { tool: MinimizedTool; onPress: (tool: MinimizedTool) => void; index: number; } function GlitchToolButton({ tool, onPress, index }: GlitchToolButtonProps) { const glitchX = useRef(new Animated.Value(0)).current; const glitchOpacity = useRef(new Animated.Value(1)).current; const glitchScale = useRef(new Animated.Value(1)).current; useEffect(() => { let isMounted = true; const runGlitch = () => { if (!isMounted) return; const d = GLITCH_DURATION_MS; Animated.parallel([ // X displacement - quick shake Animated.sequence([ Animated.timing(glitchX, { toValue: 3, duration: d * 0.2, easing: Easing.linear, useNativeDriver: true, }), Animated.timing(glitchX, { toValue: -3, duration: d * 0.2, easing: Easing.linear, useNativeDriver: true, }), Animated.timing(glitchX, { toValue: 2, duration: d * 0.2, easing: Easing.linear, useNativeDriver: true, }), Animated.timing(glitchX, { toValue: -1, duration: d * 0.2, easing: Easing.linear, useNativeDriver: true, }), Animated.timing(glitchX, { toValue: 0, duration: d * 0.2, easing: Easing.linear, useNativeDriver: true, }), ]), // Opacity flicker Animated.sequence([ Animated.timing(glitchOpacity, { toValue: 0.4, duration: d * 0.25, useNativeDriver: true, }), Animated.timing(glitchOpacity, { toValue: 1, duration: d * 0.25, useNativeDriver: true, }), Animated.timing(glitchOpacity, { toValue: 0.6, duration: d * 0.25, useNativeDriver: true, }), Animated.timing(glitchOpacity, { toValue: 1, duration: d * 0.25, useNativeDriver: true, }), ]), // Scale pulse Animated.sequence([ Animated.timing(glitchScale, { toValue: 1.1, duration: d * 0.3, useNativeDriver: true, }), Animated.timing(glitchScale, { toValue: 0.95, duration: d * 0.4, useNativeDriver: true, }), Animated.timing(glitchScale, { toValue: 1, duration: d * 0.3, useNativeDriver: true, }), ]), ]).start(); }; const scheduleNextGlitch = () => { if (!isMounted) return; const delay = MIN_GLITCH_DELAY + Math.random() * (MAX_GLITCH_DELAY - MIN_GLITCH_DELAY) + index * 300; setTimeout(() => { if (isMounted) { runGlitch(); scheduleNextGlitch(); } }, delay); }; const initialDelay = 500 + Math.random() * 1500 + index * 200; const timeoutId = setTimeout(() => { if (isMounted) { runGlitch(); scheduleNextGlitch(); } }, initialDelay); return () => { isMounted = false; clearTimeout(timeoutId); }; }, [glitchX, glitchOpacity, glitchScale, index]); return ( onPress(tool)} activeOpacity={0.7} style={styles.toolButton} accessibilityLabel={`Restore ${tool.title}`} accessibilityRole="button" > {tool.icon} ); } // ============================================================================ // Types // ============================================================================ export interface MinimizedToolsStackProps { /** Callback when a tool should be restored */ onRestore?: (tool: MinimizedTool) => void; /** Width of the parent container (for matching widths) */ containerWidth?: number; } // ============================================================================ // Main Component // ============================================================================ export function MinimizedToolsStack({ onRestore, containerWidth = TOOLBAR_WIDTH, }: MinimizedToolsStackProps) { const { minimizedTools, restore } = useMinimizedTools(); // Calculate expanded height based on number of tools const expandedHeight = TOOLBAR_PADDING + minimizedTools.length * TOOL_ITEM_SIZE + (minimizedTools.length - 1) * ICON_GAP + ICON_GAP + COLLAPSE_BUTTON_SIZE + 4; // Handle tool press (restore) const handleToolPress = useCallback( (tool: MinimizedTool) => { const restored = restore(tool.instanceId); if (restored && onRestore) { onRestore(restored); } }, [restore, onRestore] ); return ( 0} itemCount={minimizedTools.length} showCount={true} width={containerWidth} expandedHeight={expandedHeight} persistState={true} storageKey={STORAGE_KEY_EXPANDED} peekHeight={PEEK_HEIGHT} collapsedLabel={`Show ${minimizedTools.length} minimized tools`} > {/* Tool Icons - rendered in reverse order */} {[...minimizedTools].reverse().map((tool, index) => ( ))} ); } // ============================================================================ // Styles // ============================================================================ const styles = StyleSheet.create({ toolButton: { width: TOOL_ITEM_SIZE, height: TOOL_ITEM_SIZE, alignItems: "center", justifyContent: "center", backgroundColor: "transparent", }, });