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', }, })