import React from "react"; import { View, Text, Pressable, Image, Modal, StyleSheet, Dimensions, Animated } from "react-native"; import { semanticColors } from "@vendetta/ui"; import { findByProps } from "@vendetta/metro"; import { resolveSemanticColor } from "@shared/lib/color"; const TextStyleSheet = findByProps("TextStyleSheet")?.TextStyleSheet; const Haptic = findByProps("triggerHapticFeedback", "HapticFeedbackTypes"); function resolve(token: any, fallback: string): string { return resolveSemanticColor(token) ?? fallback; } const EDGE = 12; const PAD = 12; const ITEM_H = 42; const MIN_W = 220; const DIVIDER_H = 4; const RADIUS = 16; const ICON_SIZE = 20; const SPRING_CONFIG = { tension: 200, friction: 20 }; export interface ContextMenuItem { label: string; action?: () => void; danger?: boolean; iconSource?: number; IconComponent?: React.ComponentType; } export function ContextMenuModal({ visible, items, title, anchorX, anchorY, onClose, }: { visible: boolean; items: ContextMenuItem[]; title: string; anchorX: number; anchorY: number; onClose: () => void; }) { const { width: winW, height: winH } = Dimensions.get("window"); const titleH = title ? PAD + 20 + DIVIDER_H : 0; const menuH = titleH + items.length * ITEM_H; let left = anchorX; let top = anchorY + 48 + 10; if (left + MIN_W > winW - EDGE) left = winW - MIN_W - EDGE; if (left < EDGE) left = EDGE; if (top + menuH > winH - EDGE) top = anchorY - menuH - 10; if (top < EDGE) top = EDGE; const bgContainer = resolve(semanticColors?.BACKGROUND_SURFACE_HIGHEST, "#1e1f22"); const borderColor = resolve(semanticColors?.BORDER_SUBTLE, "#3f4147"); const textColor = resolve(semanticColors?.TEXT_STRONG, "#f2f3f5"); const pressedColor = resolve(semanticColors?.BACKGROUND_MOD_SUBTLE, "rgba(255,255,255,0.06)"); const backdropOpacity = React.useRef(new Animated.Value(0)).current; const menuScale = React.useRef(new Animated.Value(0.5)).current; const menuOpacity = React.useRef(new Animated.Value(0)).current; const [show, setShow] = React.useState(false); const closing = React.useRef(false); React.useEffect(() => { if (visible && !closing.current) { setShow(true); backdropOpacity.setValue(0); menuScale.setValue(0.5); menuOpacity.setValue(0); Animated.parallel([ Animated.timing(backdropOpacity, { toValue: 1, duration: 150, useNativeDriver: true }), Animated.spring(menuScale, { toValue: 1, useNativeDriver: true, ...SPRING_CONFIG }), Animated.spring(menuOpacity, { toValue: 1, useNativeDriver: true, ...SPRING_CONFIG }), ]).start(); } }, [visible]); const handleClose = React.useCallback(() => { if (closing.current) return; closing.current = true; Animated.parallel([ Animated.timing(backdropOpacity, { toValue: 0, duration: 120, useNativeDriver: true }), Animated.timing(menuScale, { toValue: 0.8, duration: 120, useNativeDriver: true }), Animated.timing(menuOpacity, { toValue: 0, duration: 120, useNativeDriver: true }), ]).start(() => { closing.current = false; setShow(false); onClose(); }); }, [onClose]); if (!show) return null; return ( {title ? ( {title} ) : null} {items.map((item, i) => { const isFirst = i === 0 && !title; const isLast = i === items.length - 1; const hasIcon = !!(item.iconSource || item.IconComponent); return ( ({ paddingHorizontal: PAD, paddingVertical: 10, minHeight: ITEM_H, flexDirection: "row", justifyContent: "space-between", alignItems: "center", gap: 8, backgroundColor: pressed ? pressedColor : "transparent", borderTopLeftRadius: isFirst ? RADIUS : 0, borderTopRightRadius: isFirst ? RADIUS : 0, borderBottomLeftRadius: isLast ? RADIUS : 0, borderBottomRightRadius: isLast ? RADIUS : 0, borderBottomWidth: !isFirst && !isLast ? 1 : 0, borderBottomColor: borderColor, })} onPress={() => { Haptic?.triggerHapticFeedback?.(Haptic.HapticFeedbackTypes.IMPACT_LIGHT); handleClose(); item.action?.(); }} > {item.label} {hasIcon ? ( {item.IconComponent ? ( ) : item.iconSource ? ( ) : null} ) : null} ); })} ); } const localStyles = StyleSheet.create({ root: { flex: 1, }, backdrop: { ...StyleSheet.absoluteFill, backgroundColor: "rgba(0,0,0,0.55)", }, container: { position: "absolute", borderWidth: 1, borderRadius: RADIUS, overflow: "hidden", elevation: 20, shadowColor: "#000", shadowOffset: { width: 0, height: 12 }, shadowOpacity: 0.24, shadowRadius: 24, }, });