← каталог
Draggable Gallery
Перетаскиваемая галерея: тяни ленту мышью или тачем, отпусти — доедет по инерции (Draggable + InertiaPlugin).
gsapdragmagneticintermediate~28 КБreduced-motion ✓
Установка
npx shadcn@latest add https://motiva.pages.dev/r/draggable-gallery.jsonзависимости: gsap@^3.15.0
Что нужно в проекте до установки
components.jsonв корне — с алиасами в форме"components": "@/components". Нет файла — CLI не поймёт, куда класть.- Алиас
@объявлен дважды:"paths": {"@/*": ["./src/*"]}в корневомtsconfig.json(в Vite он часто содержит толькоreferences— тогда CLI создаёт папку@) иresolve.aliasв конфиге сборщика. - Токены подключены один раз на приложение:
import "@/styles/motiva-tokens.css"— они приезжают вместе с записью. Без них секция потеряет цвета, ритм и тему. - Своя типографика не должна спорить: если у вас на обёртке
text-align: center, он протечёт внутрь блоков. - Проект без TypeScript? Всё равно нужен минимальный
tsconfig.jsonсpathsи"tsx": trueвcomponents.json: CLI грузит tsconfig безусловно, а с"tsx": falseидёт заjsconfig.jsonи падает так же. Приехавший рядом.tsxможно не трогать — вам нужен.vanilla.js.
Экспорт
Код
"use client";
import { useEffect, useRef } from "react";
import gsap from "gsap";
import { Draggable } from "gsap/Draggable";
import { InertiaPlugin } from "gsap/InertiaPlugin";
const SLIDES = [280, 230, 170, 60, 330, 20, 110].map((hue, i) => ({
n: String(i + 1).padStart(2, "0"),
art: `linear-gradient(150deg, oklch(0.55 0.16 ${hue} / 0.8) 0%, oklch(0.2 0.04 ${hue}) 70%), oklch(0.17 0.02 270)`,
}));
/**
* Перетаскиваемая галерея: тяни ленту мышью/тачем, отпусти — доезжает по инерции
* (GSAP Draggable + InertiaPlugin, bounds + edgeResistance). Reduced-motion — без инерции.
*/
export function DraggableGallery() {
const viewport = useRef<HTMLDivElement>(null);
const track = useRef<HTMLDivElement>(null);
useEffect(() => {
const vp = viewport.current;
const tr = track.current;
if (!vp || !tr) return;
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const frozen = document.documentElement.hasAttribute("data-shot") || reduce;
gsap.registerPlugin(Draggable, InertiaPlugin);
const [drag] = Draggable.create(tr, {
type: "x",
bounds: vp,
inertia: !frozen,
edgeResistance: 0.72,
cursor: "grab",
activeCursor: "grabbing",
});
return () => {
drag?.kill();
};
}, []);
return (
<div style={{ width: "100vw", overflowX: "clip", color: "oklch(0.92 0.01 265)" }}>
<div style={{ textAlign: "center", marginBottom: 14, fontFamily: "ui-monospace, monospace", fontSize: "0.75rem", opacity: 0.55 }}>
⇠ тяни ленту ⇢
</div>
<div ref={viewport} style={{ overflow: "visible", paddingInline: "6vw" }}>
<div
ref={track}
style={{
display: "flex",
gap: 18,
width: "max-content",
touchAction: "pan-y",
}}
>
{SLIDES.map((s) => (
<figure
key={s.n}
style={{
margin: 0,
flexShrink: 0,
width: 190,
height: 250,
borderRadius: 16,
border: "1px solid oklch(0.3 0.02 270)",
background: s.art,
display: "flex",
alignItems: "flex-end",
padding: "0.9rem",
userSelect: "none",
}}
>
<figcaption style={{ fontFamily: "ui-monospace, monospace", fontSize: "0.8rem", opacity: 0.75 }}>
{s.n}
</figcaption>
</figure>
))}
</div>
</div>
</div>
);
}
import gsap from "gsap";
import { Draggable } from "gsap/Draggable";
import { InertiaPlugin } from "gsap/InertiaPlugin";
/**
* Draggable Gallery — vanilla JS/GSAP (без React). Тяни ленту, отпусти — доедет по инерции.
*
* <div data-dg-viewport style="overflow:visible">
* <div data-dg-track style="display:flex;gap:18px;width:max-content;touch-action:pan-y">…карточки…</div>
* </div>
* <script type="module">
* import { initDraggableGallery } from './draggable-gallery.vanilla.js';
* const destroy = initDraggableGallery(document.querySelector('[data-dg-viewport]'));
* </script>
*
* @param {HTMLElement} viewport — контейнер-границы с [data-dg-track] внутри
* @returns {() => void} destroy
*/
export function initDraggableGallery(viewport) {
const track = viewport.querySelector("[data-dg-track]");
if (!track) return () => {};
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const frozen = document.documentElement.hasAttribute("data-shot") || reduce;
gsap.registerPlugin(Draggable, InertiaPlugin);
const [drag] = Draggable.create(track, {
type: "x",
bounds: viewport,
inertia: !frozen, // reduced-motion: без инерционного доезда
edgeResistance: 0.72,
cursor: "grab",
activeCursor: "grabbing",
});
return () => drag?.kill();
}
Похожие эффекты
лицензия: MITавтор: Motivaисточник: оригинал (Motiva); паттерн — GSAP Draggable demosv1.0.0