import { View, Text, ScrollView, StyleSheet, Dimensions, TouchableOpacity, } from "react-native"; import React, { useEffect, useMemo, useState } from "react"; import Animated, { runOnJS, useAnimatedGestureHandler, useAnimatedStyle, useSharedValue, withSpring, } from "react-native-reanimated"; import { PanGestureHandler } from "react-native-gesture-handler"; const { width } = Dimensions.get("window"); // Child component to respect Rules of Hooks (no hooks in loops) const DraggableItem = ({ item, gestureHandler, draggingItem, translateX, translateY, onActivate }) => { const style = useAnimatedStyle(() => ({ transform: [ { translateX: draggingItem.value === item.id ? translateX.value : 0 }, { translateY: draggingItem.value === item.id ? translateY.value : 0 }, ], zIndex: draggingItem.value === item.id ? 10 : 0, })); return ( onActivate(item.id)} > {item.label ?? String(item)} ); }; // Props: // - sourceItems: array of { id, label, value } or strings // - initialSelected: array of ids to prefill right side (optional) // - onChange: callback with array of values (or strings) in right order const SongStructureDragDrop = ({ sourceItems, initialSelected, onChange }) => { const normalize = (arr = []) => arr.map((item, idx) => typeof item === "string" ? { id: `${item}-${idx}`, label: item, value: item } : item, ); const normalizedSource = useMemo( () => normalize(sourceItems || []), [sourceItems], ); const [rightItems, setRightItems] = useState([]); // Compare arrays by item id and order to avoid unnecessary state churn const sameById = (a = [], b = []) => { if (a === b) return true; if (!a || !b) return false; if (a.length !== b.length) return false; for (let idx = 0; idx < a.length; idx++) { if (a[idx]?.id !== b[idx]?.id) return false; } return true; }; useEffect(() => { const selected = Array.isArray(initialSelected) ? initialSelected : []; const selectedSet = new Set(selected); const nextRight = normalizedSource.filter((i) => selectedSet.has(i.id)); setRightItems((prev) => (sameById(prev, nextRight) ? prev : nextRight)); }, [normalizedSource, initialSelected]); const leftItems = useMemo(() => { if (!rightItems || rightItems.length === 0) return normalizedSource; const rightIds = new Set(rightItems.map((item) => item.id)); return normalizedSource.filter((item) => !rightIds.has(item.id)); }, [normalizedSource, rightItems]); const draggingItem = useSharedValue(null); const translateX = useSharedValue(0); const translateY = useSharedValue(0); const onActivate = (id) => { draggingItem.value = id; }; const moveItem = (itemId) => { const item = normalizedSource.find((i) => i.id === itemId); if (!item) return; setRightItems((current) => { if (current.some((i) => i.id === itemId)) return current; const next = [...current, item]; onChange?.(next.map((i) => i.value ?? i.label)); return next; }); }; const removeItem = (itemId) => { setRightItems((current) => { if (!current.some((i) => i.id === itemId)) return current; const next = current.filter((i) => i.id !== itemId); onChange?.(next.map((i) => i.value ?? i.label)); return next; }); }; const gestureHandler = useAnimatedGestureHandler({ onStart: (_, ctx) => { ctx.startX = translateX.value; ctx.startY = translateY.value; }, onActive: (event, ctx) => { translateX.value = ctx.startX + event.translationX; translateY.value = ctx.startY + event.translationY; }, onEnd: () => { if (translateX.value > width / 2) { runOnJS(moveItem)(draggingItem.value); } translateX.value = withSpring(0); translateY.value = withSpring(0); draggingItem.value = null; }, }); return ( Left {leftItems.map((item) => ( ))} Right Appuyez pour retirer un élément {rightItems.map((item) => ( removeItem(item.id)} activeOpacity={0.7} accessibilityRole="button" accessibilityLabel={`Retirer ${item.label ?? String(item)}`} > {item.label ?? String(item)} Retirer ))} ); }; export default SongStructureDragDrop; const styles = StyleSheet.create({ scrollWrapper: { flexDirection: "row", justifyContent: "space-around", }, scroll: { width: width / 2.2, height: "90%", backgroundColor: "#f0f0f0", borderRadius: 10, margin: 5, padding: 10, }, title: { fontWeight: "bold", fontSize: 16, marginBottom: 10, }, item: { backgroundColor: "#aaf", padding: 15, marginVertical: 5, borderRadius: 10, }, selectedItem: { backgroundColor: "#bdf", padding: 15, marginVertical: 5, borderRadius: 10, alignItems: "center", }, helpText: { fontSize: 12, color: "#666", marginBottom: 8, textAlign: "center", }, removeHint: { fontSize: 12, color: "#555", marginTop: 4, }, text: { color: "#333", textAlign: "center", }, });