Вокруг кнопки есть невидимое поле радиусом range. Пока курсор в нём, кнопка смещается к нему на долю расстояния strength, а подпись — ещё чуть дальше (contentStrength), отчего кнопка кажется объёмной. Курсор ушёл — кнопка пружинит обратно с лёгким перелётом.
На телефоне магнит работает от пальца, а нажатие даёт отклик: кнопка сжимается до pressScale и срабатывает лёгкая вибрация (haptics, через expo-haptics; в вебе вибрация пропускается).
Поставьте зависимости:
npm install expo-haptics react-native-gesture-handler react-native-reanimated react-native-workletspnpm add expo-haptics react-native-gesture-handler react-native-reanimated react-native-workletsyarn add expo-haptics react-native-gesture-handler react-native-reanimated react-native-workletsbun add expo-haptics react-native-gesture-handler react-native-reanimated react-native-workletsСкопируйте эти файлы в src/shared/components/micro-interactions/magnetic-button/:
Проверьте алиас путей. Импорты в коде выше записаны так, как их пишет astra-rn add: @/shared/components, @/shared/utils и @/shared/hooks. Нужен алиас "@/*": ["./src/*"] в tsconfig.json — или поправьте импорты под свои пути.
import { useRef, useState } from "react";
import { StyleSheet, Text, View } from "react-native";
import { StatusBar } from "expo-status-bar";
import { Ionicons } from "@expo/vector-icons";
import { MagneticButton, type MagneticButtonHandle } from "@/shared/components/micro-interactions/magnetic-button";
import { Showcase, useAutoplay, useCustomize } from "~/showcase";
const CUSTOMIZE = [
{ key: "label", label: "Текст кнопки", type: "text", default: "Начать", maxLength: 16 },
{ key: "strength", label: "Сила притяжения", type: "slider", min: 0, max: 0.8, step: 0.05, default: 0.35 },
{ key: "contentStrength", label: "Сдвиг содержимого", type: "slider", min: 0, max: 0.6, step: 0.02, default: 0.18 },
{ key: "range", label: "Радиус магнита", type: "slider", min: 20, max: 140, step: 2, unit: "px", default: 70 },
{ key: "pressScale", label: "Сжатие при нажатии", type: "slider", min: 0.8, max: 1, step: 0.01, default: 0.93 },
{ key: "radius", label: "Скругление", type: "slider", min: 0, max: 32, step: 1, unit: "px", default: 20 },
{ key: "color", label: "Цвет кнопки", type: "color", default: "#f5f2ef" },
{ key: "textColor", label: "Цвет текста", type: "color", default: "#0a0a0a" },
] as const;
export default function App() {
const custom = useCustomize(CUSTOMIZE);
const primary = useRef<MagneticButtonHandle>(null);
const [count, setCount] = useState(0);
useAutoplay(async ({ wait }) => {
for (const [x, y] of [
[-90, -40],
[80, -50],
[95, 45],
[-70, 55],
] as const) {
primary.current?.attract(x, y);
await wait(520);
}
primary.current?.release();
await wait(1000);
});
return (
<Showcase>
<View style={styles.container}>
<StatusBar style="light" />
<MagneticButton
ref={primary}
label={custom.label}
strength={custom.strength}
contentStrength={custom.contentStrength}
range={custom.range}
pressScale={custom.pressScale}
radius={custom.radius}
color={custom.color}
textColor={custom.textColor}
onPress={() => setCount((value) => value + 1)}
/>
<MagneticButton
width={64}
height={64}
radius={32}
range={custom.range * 0.8}
strength={custom.strength}
contentStrength={custom.contentStrength}
pressScale={custom.pressScale}
color="#1c1c1c"
label="Избранное"
onPress={() => setCount((value) => value + 1)}
>
<Ionicons name="heart" size={24} color="#f5f2ef" />
</MagneticButton>
<Text style={styles.counter}>Нажатий: {count}</Text>
</View>
</Showcase>
);
}
const styles = StyleSheet.create({
container: { flex: 1, alignItems: "center", justifyContent: "center", overflow: "hidden" },
counter: { color: "#8a8683", fontSize: 13, fontVariant: ["tabular-nums"] },
});label?Тип string
children?Тип ReactNode
onPress?Тип () => void
width?Тип number
height?Тип number
radius?Тип number
range?Тип number
strength?Тип number
contentStrength?Тип number
pressScale?Тип number
haptics?Тип boolean
color?Тип string
textColor?Тип string
disabled?Тип boolean
style?Тип StyleProp<ViewStyle>
textStyle?Тип StyleProp<TextStyle>
attractТип (x: number, y: number) => void
releaseТип () => void