Motiva
Мои коллекции
← каталог

Draggable Gallery

Перетаскиваемая галерея: тяни ленту мышью или тачем, отпусти — доедет по инерции (Draggable + InertiaPlugin).

gsapdragmagneticintermediate~28 КБreduced-motion ✓
↗

Установка

npx shadcn@latest add https://motiva.pages.dev/r/draggable-gallery.json

зависимости: gsap@^3.15.0

Что нужно в проекте до установки
  1. Файл components.json в корне

    Из него CLI узнаёт, куда класть файлы. Алиасы — в такой форме:

    "aliases": { "components": "@/components", "lib": "@/lib" }

    Нет файла — установка не начнётся вовсе.

  2. Алиас @ объявлен дважды

    В корневом tsconfig.json и в конфиге сборщика. В Vite корневой tsconfig часто содержит только references — этого мало.

    "paths": { "@/*": ["./src/*"] }        // tsconfig.json
    resolve.alias = { "@": "/src" }        // vite.config.ts

    Без paths CLI молча создаёт папку с именем @.

  3. Токены подключены один раз на приложение

    Они приезжают вместе с записью — достаточно импортировать их в точке входа:

    import "@/styles/motiva-tokens.css";

    Без них блок потеряет цвета, ритм и тему.

  4. Своя типографика не спорит с блоком

    Наследуемые свойства протекают внутрь: text-align: center на обёртке отцентрует содержимое секции. То же касается font-familyи line-height.

  5. Проект без TypeScript? всё равно нужен tsconfig

    CLI грузит tsconfig.json безусловно, а при "tsx": false идёт за jsconfig.json и падает так же. Минимум — файл с pathsи "tsx": true в components.json.

    Приехавший рядом .tsx можно не трогать — вам нужен .vanilla.js.

Экспорт

↓ Скачать .zip

Код

"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>
  );
}
лицензия: MITавтор: Motivaисточник: оригинал (Motiva)v1.0.0