Поставьте зависимости:
npm install expo-blur react-native-reanimatedpnpm add expo-blur react-native-reanimatedyarn add expo-blur react-native-reanimatedbun add expo-blur react-native-reanimatedСкопируйте эти файлы в src/shared/components/molecules/tilt-carousel/:
Проверьте алиас путей. Импорты в коде выше записаны так, как их пишет astra-rn add: @/shared/components, @/shared/utils и @/shared/hooks. Нужен алиас "@/*": ["./src/*"] в tsconfig.json — или поправьте импорты под свои пути.
import { useEffect, useRef } from "react";
import { View, StyleSheet, Image, Platform } from "react-native";
import { StatusBar } from "expo-status-bar";
import { TiltCarousel } from "@/shared/components/molecules/tilt-carousel";
import { Showcase, useAutoplay, useCustomize } from "~/showcase";
import { PHOTO_SOURCES } from "~/assets/photos";
const ITEM_WIDTH = 350;
const ITEM_MARGIN = 20;
const STEP = ITEM_WIDTH + ITEM_MARGIN * 2;
const PHOTOS = [
PHOTO_SOURCES["tilt-carousel-1"],
PHOTO_SOURCES["tilt-carousel-2"],
PHOTO_SOURCES["tilt-carousel-3"],
PHOTO_SOURCES["tilt-carousel-4"],
];
const CUSTOMIZE = [
{ key: "photos", label: "Изображения", type: "images", default: PHOTOS },
{ key: "rotationAngle", label: "Угол наклона", type: "slider", min: 0, max: 80, step: 1, unit: "°", default: 30 },
{ key: "translateY", label: "Сдвиг вниз", type: "slider", min: 0, max: 200, step: 1, unit: "px", default: 55 },
{ key: "itemHeight", label: "Высота карточки", type: "slider", min: 250, max: 650, step: 5, unit: "px", default: 500 },
{
key: "origin",
label: "Ось наклона",
type: "select",
options: [
{ value: "top", label: "Верх" },
{ value: "center", label: "Центр" },
{ value: "bottom", label: "Низ" },
],
default: "bottom",
},
{ key: "blur", label: "Размытие", type: "toggle", default: true },
{ key: "radius", label: "Скругление", type: "slider", min: 0, max: 60, step: 1, unit: "px", default: 20 },
{ key: "borderColor", label: "Цвет рамки", type: "color", default: "#1a1a1a" },
{ key: "pace", label: "Темп автопоказа", type: "slider", min: 0.25, max: 3, step: 0.05, default: 1 },
] 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 smoothScrollTo(scroller: HTMLElement, left: number) {
HTMLElement.prototype.scrollTo.call(scroller, { left, behavior: "smooth" });
}
function useWebDragCarousel(step: number, count: number) {
const hostRef = useRef<View>(null);
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();
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)),
);
smoothScrollTo(scroller, index * step);
};
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);
};
}, [step, count]);
const goTo = (index: number) => {
const scroller = findScroller(
hostRef.current as unknown as HTMLElement | null,
);
if (scroller) smoothScrollTo(scroller, index * step);
};
return { hostRef, goTo };
}
interface Assets {
id: string;
image: string;
}
export default function App() {
const custom = useCustomize(CUSTOMIZE);
const assets: Assets[] = custom.photos.map((image, index) => ({
id: String(index + 1),
image,
}));
const paceRef = useRef(custom.pace);
paceRef.current = custom.pace;
const { hostRef, goTo } = useWebDragCarousel(STEP, assets.length);
useAutoplay(async ({ wait }) => {
goTo(0);
await wait(700 / paceRef.current);
for (const index of [1, 2, 3, 1, 0]) {
goTo(index);
await wait(520 / paceRef.current);
}
await wait(200 / paceRef.current);
});
return (
<Showcase>
<View style={styles.container}>
<StatusBar style="light" />
<View ref={hostRef} style={styles.carouselContainer}>
<TiltCarousel
data={assets}
itemWidth={ITEM_WIDTH}
itemHeight={custom.itemHeight}
marginHorizontal={ITEM_MARGIN}
rotationAngle={custom.rotationAngle}
translateYValue={custom.translateY}
transformOrigin={custom.origin as "top" | "center" | "bottom"}
useBlur={custom.blur}
renderItem={({ item }) => (
<View
style={[
styles.card,
{ borderRadius: custom.radius, borderColor: custom.borderColor },
]}
>
<Image
source={{ uri: item.image }}
style={styles.image}
resizeMode="cover"
/>
</View>
)}
keyExtractor={(item) => item.id}
/>
</View>
</View>
</Showcase>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#0a0a0a",
justifyContent: "center",
alignItems: "center",
},
carouselContainer: {
marginTop: 0,
},
card: {
flex: 1,
backgroundColor: "#000",
borderRadius: 20,
overflow: "hidden",
borderWidth: 1,
borderColor: "#1a1a1a",
},
image: {
width: "100%",
height: "100%",
},
});dataТип readonly T[]
renderItemТип (info: ICarouselRenderItem<T>) => React.ReactNode
keyExtractor?Тип (item: T, index: number) => string
itemWidth?Тип number
itemHeight?Тип number
marginHorizontal?Тип number
rotationAngle?Тип number
translateYValue?Тип number
transformOrigin?Тип "bottom" | "center" | "top"
useBlur?Тип boolean
scrollEventThrottle?Тип number
decelerationRate?Тип "fast" | "normal" | number
showsHorizontalScrollIndicator?Тип boolean
itemТип T
indexТип number
scrollXТип SharedValue<number>
renderItemТип (info: ICarouselRenderItem<T>) => React.ReactNode
itemWidthТип number
itemHeightТип number
marginHorizontalТип number
fullWidthТип number
rotationAngleТип number
translateYValueТип number
transformOriginТип "bottom" | "center" | "top"
useBlurТип boolean
itemТип T
indexТип number
scrollXТип SharedValue<number>
itemWidthТип number
itemHeightТип number
marginHorizontalТип number
fullWidthТип number