"use client"; // Süzülen haplar (DESIGN.md §4.30): kataloğun en çok aranan bölüm ve şehir // sayfalarına giden haplar. Bölüm ekrana girince haplar yukarıdan hafif // eğik düşüp yerine oturur; imleç gezdikçe her hap kendi derinliğinde azıcık // süzülür, arkadaki piksel kareler ters yöne kayar. Her hap gerçek bir // linktir (§4.12 kimlik hapı, hover tek turuncu). // // Esin: React Bits "Parallax Pills" (imleci izleyen, sekerek giren etiketli // haplar). Kod bize ait: haplar mutlak konumla değil akışla dizilir (375 px'te // üst üste binmez), giriş tek ve hafif sekmeli (düşen nesne, §6), arka plan // süs hapı yerine sitenin piksel kareleri. Dokunmatikte süzülme yoktur; // hareketi azalt'ta haplar yalnız belirir. import { useEffect, useRef, useState } from "react"; import Link from "next/link"; import { motion, useMotionValue, useReducedMotion, useSpring, useTransform, type MotionValue, } from "motion/react"; import { GraduationCap, MapPin } from "lucide-react"; import { cn } from "@/lib/utils"; import type { KatalogHapi } from "@/features/pazarlama/landing-verisi"; /** İmlecin kenardan kenara taşıdığı en büyük kayma (px). */ const GUC = 18; const SUZULME_YAYI = { stiffness: 120, damping: 20, mass: 0.6 }; /** Düşüş: yukarıdan gelir, tek kez hafif seker. */ const DUSUS = { type: "spring", duration: 0.7, bounce: 0.22 } as const; const karma = (i: number, tohum: number) => { const n = Math.sin(i * 127.1 + tohum * 311.7) * 43758.5453; return n - Math.floor(n); }; // Arkadaki piksel kareler: kenarlarda, orta boş (§10.8 doku kuralı). Konum %. const KARELER = [ { x: 4, y: 12, boy: 27, sicak: false }, { x: 13, y: 78, boy: 17, sicak: true }, { x: 24, y: 2, boy: 17, sicak: false }, { x: 77, y: 0, boy: 27, sicak: false }, { x: 90, y: 22, boy: 17, sicak: true }, { x: 94, y: 70, boy: 37, sicak: false }, { x: 70, y: 90, boy: 17, sicak: false }, { x: 0, y: 52, boy: 17, sicak: false }, ]; type Durum = "ssr" | "bekliyor" | "geldi"; function Hap({ hap, sira, durum, azalt, mx, my, }: { hap: KatalogHapi; sira: number; durum: Durum; azalt: boolean; mx: MotionValue; my: MotionValue; }) { // Her hap kendi derinliğinde: öndekiler çok, arkadakiler az kayar const derinlik = 0.45 + karma(sira, 5) * 0.75; const x = useTransform(mx, (v) => v * GUC * derinlik); const y = useTransform(my, (v) => v * GUC * 0.7 * derinlik); const egim = (karma(sira, 9) - 0.5) * 16; const iri = sira % 3 === 0; const Ikon = hap.tur === "sehir" ? MapPin : GraduationCap; const gizli = azalt ? { opacity: 0 } : { opacity: 0, y: -36, rotate: egim }; return ( {hap.etiket} {hap.tur === "sehir" ? " üniversiteleri" : " taban puanları"} ); } function ArkaKare({ kare, mx, my, }: { kare: (typeof KARELER)[number]; mx: MotionValue; my: MotionValue; }) { // Arka katman ters yöne ve daha çok kayar: derinlik hissi const x = useTransform(mx, (v) => -v * GUC * 1.4); const y = useTransform(my, (v) => -v * GUC); return ( ); } export function SuzulenHaplar({ haplar, className, }: { haplar: KatalogHapi[]; className?: string; }) { const kapRef = useRef(null); const azalt = useReducedMotion() ?? false; // SSR'da haplar görünür çizilir (JS'siz ve bot için). Montajdan sonra bölüm // henüz ekranda değilse gizlenir, girince düşer; zaten ekrandaysa dokunulmaz. const [durum, setDurum] = useState("ssr"); const hamX = useMotionValue(0); const hamY = useMotionValue(0); const mx = useSpring(hamX, SUZULME_YAYI); const my = useSpring(hamY, SUZULME_YAYI); useEffect(() => { const kap = kapRef.current; if (!kap || typeof IntersectionObserver === "undefined") return; let ilk = true; const io = new IntersectionObserver( ([kayit]) => { if (kayit.isIntersecting) { setDurum((d) => (d === "bekliyor" ? "geldi" : d)); io.disconnect(); } else if (ilk) { setDurum("bekliyor"); } ilk = false; }, { rootMargin: "0px 0px -80px 0px" }, ); io.observe(kap); return () => io.disconnect(); }, []); // İmleç: yalnız fare; kabın ortasına göre −0,5…0,5 useEffect(() => { const kap = kapRef.current; if (!kap || azalt || !window.matchMedia("(hover: hover) and (pointer: fine)").matches) { return; } const oynadi = (e: PointerEvent) => { const kutu = kap.getBoundingClientRect(); hamX.set(Math.max(-0.5, Math.min(0.5, (e.clientX - kutu.left) / kutu.width - 0.5))); hamY.set(Math.max(-0.5, Math.min(0.5, (e.clientY - kutu.top) / kutu.height - 0.5))); }; const cikti = () => { hamX.set(0); hamY.set(0); }; kap.addEventListener("pointermove", oynadi, { passive: true }); kap.addEventListener("pointerleave", cikti); return () => { kap.removeEventListener("pointermove", oynadi); kap.removeEventListener("pointerleave", cikti); }; }, [azalt, hamX, hamY]); return (
{KARELER.map((kare) => ( ))}
    {haplar.map((hap, i) => (
  • ))}
); }