Поставьте зависимости:
npm install expo-blur expo-haptics react-native-reanimated react-native-workletspnpm add expo-blur expo-haptics react-native-reanimated react-native-workletsyarn add expo-blur expo-haptics react-native-reanimated react-native-workletsbun add expo-blur expo-haptics react-native-reanimated react-native-workletsСкопируйте эти файлы в src/shared/components/molecules/circular-list/:
Проверьте алиас путей. Импорты в коде выше записаны так, как их пишет astra-rn add: @/shared/components, @/shared/utils и @/shared/hooks. Нужен алиас "@/*": ["./src/*"] в tsconfig.json — или поправьте импорты под свои пути.
import { useEffect, useRef } from "react";
import { View, Text, StyleSheet, Dimensions, Platform } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { StatusBar } from "expo-status-bar";
import { useFonts } from "expo-font";
import { SymbolView } from "expo-symbols";
import CircularList from "@/shared/components/molecules/circular-list";
import { Showcase, useAutoplay, useCustomize } from "~/showcase";
import { PHOTO_SOURCES } from "~/assets/photos";
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get("window");
const ITEM_STEP = SCREEN_WIDTH / 4;
const PHOTOS = [
PHOTO_SOURCES["circular-list-1"],
PHOTO_SOURCES["circular-list-2"],
PHOTO_SOURCES["circular-list-3"],
PHOTO_SOURCES["circular-list-4"],
PHOTO_SOURCES["circular-list-5"],
PHOTO_SOURCES["circular-list-6"],
PHOTO_SOURCES["circular-list-7"],
];
const CUSTOMIZE = [
{ key: "photos", label: "Изображения", type: "images", default: PHOTOS },
{ key: "scaleEnabled", label: "Масштаб центра", type: "toggle", default: false },
{ key: "reverse", label: "Обратная дуга", type: "toggle", default: false },
{ key: "step", label: "Шаг автопоказа", type: "slider", min: 400, max: 3000, step: 50, unit: "ms", default: 1000 },
] as const;
function findScroller(host: HTMLElement | null) {
if (!host) return null;
return (
[host, ...Array.from(host.querySelectorAll<HTMLElement>("*"))].find(
(node) => /auto|scroll/.test(getComputedStyle(node).overflowX),
) ?? null
);
}
function animateScroll(scroller: HTMLElement, left: number, frame: { id: number }) {
cancelAnimationFrame(frame.id);
const from = scroller.scrollLeft;
const start = performance.now();
const tick = () => {
const progress = Math.min(1, (performance.now() - start) / 450);
scroller.scrollLeft = from + (left - from) * (1 - Math.pow(1 - progress, 3));
if (progress < 1) frame.id = requestAnimationFrame(tick);
};
frame.id = requestAnimationFrame(tick);
}
function useWebDragCarousel(step: number, count: number) {
const hostRef = useRef<View>(null);
const frame = useRef({ id: 0 });
useEffect(() => {
if (Platform.OS !== "web") return;
const host = hostRef.current as unknown as HTMLElement | null;
const scroller = findScroller(host);
if (!host || !scroller) return;
let startX = 0;
let startLeft = 0;
let lastX = 0;
let lastTime = 0;
let velocity = 0;
let dragging = false;
const onDown = (event: MouseEvent) => {
if (event.button !== 0) return;
event.preventDefault();
cancelAnimationFrame(frame.current.id);
dragging = true;
startX = lastX = event.clientX;
startLeft = scroller.scrollLeft;
lastTime = performance.now();
velocity = 0;
};
const onMove = (event: MouseEvent) => {
if (!dragging) return;
const now = performance.now();
velocity = (event.clientX - lastX) / Math.max(1, now - lastTime);
lastX = event.clientX;
lastTime = now;
scroller.scrollLeft = startLeft - (event.clientX - startX);
};
const onUp = () => {
if (!dragging) return;
dragging = false;
const projected = scroller.scrollLeft - velocity * 180;
const index = Math.max(0, Math.min(count - 1, Math.round(projected / step)));
animateScroll(scroller, index * step, frame.current);
};
host.addEventListener("mousedown", onDown);
window.addEventListener("mousemove", onMove);
window.addEventListener("mouseup", onUp);
return () => {
host.removeEventListener("mousedown", onDown);
window.removeEventListener("mousemove", onMove);
window.removeEventListener("mouseup", onUp);
cancelAnimationFrame(frame.current.id);
};
}, [step, count]);
const goTo = (index: number) => {
const scroller = findScroller(hostRef.current as unknown as HTMLElement | null);
if (scroller) animateScroll(scroller, index * step, frame.current);
};
return { hostRef, goTo };
}
export default function App() {
const [fontLoaded] = useFonts({
SfProRounded: require("@/assets/fonts/sf-pro-rounded.ttf"),
HelveticaNowDisplay: require("@/assets/fonts/HelveticaNowDisplayMedium.ttf"),
});
const custom = useCustomize(CUSTOMIZE);
const CONTACTS = custom.photos;
const { hostRef, goTo } = useWebDragCarousel(ITEM_STEP, CONTACTS.length);
useAutoplay(async ({ wait }) => {
goTo(0);
await wait(900);
for (const index of [2, 4, 6, 3, 0]) {
goTo(index);
await wait(custom.step);
}
});
return (
<Showcase>
<StatusBar style="light" />
<View ref={hostRef} style={styles.listContainer}>
<CircularList
data={CONTACTS}
scaleEnabled={custom.scaleEnabled}
reverse={custom.reverse}
/>
</View>
</Showcase>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#0a0a0a",
},
header: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 20,
paddingTop: 70,
},
backButton: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: "#1a1a1a",
justifyContent: "center",
alignItems: "center",
},
title: {
fontSize: 17,
fontWeight: "600",
color: "#fff",
},
placeholder: {
width: 40,
},
listContainer: {
height: SCREEN_WIDTH * 0.6,
marginTop: SCREEN_HEIGHT * 0.35,
},
actions: {
alignItems: "center",
gap: 24,
paddingHorizontal: 24,
},
actionRow: {
flexDirection: "row",
alignItems: "center",
gap: 20,
},
actionButton: {
width: 56,
height: 56,
borderRadius: 28,
backgroundColor: "#1a1a1a",
justifyContent: "center",
alignItems: "center",
},
callButton: {
width: 72,
height: 72,
borderRadius: 36,
backgroundColor: "#30d158",
},
hint: {
fontSize: 13,
color: "#444",
},
});dataТип string[]
scaleEnabled?Тип boolean
reverse?Тип boolean
indexТип number
imageUriТип string
contentOffsetТип SharedValue<number>
reverseТип boolean
scaleEnabled?Тип boolean