Hasil akhir · tombol yang ikut mendekat
Dua web yang sama, bedanya satu snippet. Di kanan, tombol Ceritakan usahamu tertarik ke arah kursor yang mendekat, lalu kembali ke tempatnya. Di HP efek ini tidak ada, dan tombolnya tetap bisa ditekan seperti biasa.
Langkah 1 · Siapkan dulu
Mencari, melihat pratinjau, dan perencanaan lewat agent tidak memakai kuota.
Langkah 2 · Jelajahi katalog
Buka asset.inwebsite.online. Katalognya berisi 172 snippet. Persempit lewat kategori, tempat pemakaian, atau kata kunci.
Langkah 3 · Buka halaman snippet
npm i gsap @gsap/react. Syaratnya React + Tailwind CSS.Langkah 4 · Ambil kodenya
Agent mencari, memilih, dan memasang snippet sendiri. Buka halaman For agents, login, buat API key, lalu salin perintahnya.
Perintah di atas untuk Claude Code. Halaman itu juga punya versi Claude Desktop, Cursor, dan VS Code. Jangan pernah membagikan API key-mu.
Mencari dan perencanaan tanpa kunci. Kodenya (get_snippet) baru keluar dengan API key, dan memakai jatah 5 salinan akun gratis.
Login, buka halaman snippet, tekan Copy di tab Code. Simpan sebagai komponen, lalu pasang sendiri atau kirim ke agent.
Simpan sebagai components/MagneticButton.jsx. Tiap Copy memakai 1 dari 5 jatah harian akun gratis.
Langkah 5 · Beri konteks ke agent
Snippet aslinya kartu demo berisi judul, tiga tombol, dan kursor kustom. Di web contoh yang diganti cuma tombol CTA di hero, jadi agent perlu tahu tombolnya, apa yang terjadi saat diklik, dan apa yang dibuang. Tanpa itu, kartu "Ready when you are." bisa ikut muncul di hero.
Ini kode komponen dari asset.inwebsite.online. Pakai untuk tombol CTA di hero (src/sections/Hero.jsx, di dalam .hero__actions), menggantikan tautan "Ceritakan usahamu →". Teks, href="#kontak", dan onClick lama (preventDefault lalu scrollToId("kontak")) harus tetap. Buang kartu, judul, latar titik, kursor kustom, dan self-demo; yang dipakai cuma tombol dan fisika magnetnya. Bentuk dan ukuran sama dengan .btn .btn--accent (pil, padding 14px 22px, 16px), warna aksen #e8461e, saat disorot jadi #141311. Efek magnet hanya untuk mouse/trackpad: cek matchMedia("(hover: hover) and (pointer: fine)"), di layar sentuh tombol tetap tombol biasa. Ganti ease elastic jadi power3.out. Proyek saya React + CSS biasa tanpa Tailwind: pasang Tailwind v4 tanpa preflight supaya gaya lama tidak berubah.
<tempel kode di sini>
# sekali saja: sambungkan MCP $ claude mcp add --transport http InWebAsset https://asset.inwebsite.online/mcp --header "Authorization: Bearer <API key-mu>" # masuk ke folder proyek, jalankan agent $ claude > Pakai InWebAsset MCP. Ambil snippet micro-motion/magnetic-button dan pakai untuk tombol CTA di hero (src/sections/Hero.jsx, .hero__actions), menggantikan tautan "Ceritakan usahamu →". Teks, href="#kontak", dan onClick lama (scrollToId("kontak")) tetap. Yang dipakai cuma tombol dan fisika magnetnya: buang kartu demo, judul, kursor kustom, dan self-demo. Efek hanya untuk mouse/trackpad (cek pointer: fine); di HP tombol tetap biasa. Proyek saya React + CSS biasa tanpa Tailwind, jadi pasang Tailwind v4 tanpa preflight. Kirim look situs (terang, aksen #e8461e, sudut bulat, gerak tenang, tempo sedang) ke get_snippet dan terapkan saran "Fit this site". Cek dulu dengan includeCode:false, lalu ambil kodenya sekali. Jangan ubah bagian lain.
Yang kamu dapat
Kode di kanan adalah komponen yang terpasang di web contoh. Ini yang diubah dari versi asli di katalog:
kartu demokartu krem, judul, latar titik, tiga kursor kustom, dan self-demo (autoPlay) dibuang. Kursor bergambar kartun terlalu ramai untuk situs ini.variansolid/outline/pill jadi gaya situs accent dan line. Hero ini hanya punya satu CTA, jadi yang terpasang satu tombol.buttonjadi <a href="#kontak"> dengan onClick lama, jadi tetap turun ke form kontak.warnaungu, kuning, dan limau jadi aksen #e8461e, saat disorot #141311. Ukuran dan bentuknya sama dengan tombol lama.jangkauankursor dibaca di seluruh jendela, bukan hanya di dalam kartu demo, jadi tarikan mulai 72 px sebelum tombol tersentuh.layar sentuhmagnet hanya aktif bila (hover: hover) and (pointer: fine) cocok. Di HP tidak ada listener dan transform, jadi tap langsung menjalankan onClick. Efeknya bonus untuk desktop.easeelastic.out jadi power3.out dan durasi ×1,25, sesuai saran "Fit this site". Redaman pegas 11 → 13 supaya pantulannya lebih tenang.Tailwindweb contoh tidak memakai Tailwind. Dipasang v4 tanpa preflight (hanya theme + utilities) agar gaya lama tetap./*
* Magnetic Button — diadaptasi untuk Tandarupa
* Asal: https://asset.inwebsite.online/snippets/micro-motion/magnetic-button (v1.0.1)
* Perubahan: dipakai untuk tombol CTA hero (tautan <a> lama tetap), warna aksen #e8461e,
* tanpa kartu/judul/kursor kustom/self-demo, ease elastic → power3.out, efek hanya aktif
* di perangkat dengan mouse atau trackpad. Di layar sentuh tombol tetap tombol biasa.
*/
import { useRef } from 'react'
import gsap from 'gsap'
import { useGSAP } from '@gsap/react'
gsap.registerPlugin(useGSAP)
// Magnet hanya untuk pointer yang bisa melayang (mouse, trackpad). HP dan tablet: tombol biasa.
const FINE_POINTER = '(hover: hover) and (pointer: fine)'
const VARIANT_CLASS = {
accent:
'border-transparent bg-[#e8461e] text-[#f1ede4] shadow-[0_14px_30px_-14px_rgba(232,70,30,0.85)] hover:bg-[#141311] hover:shadow-[0_14px_30px_-14px_rgba(20,19,17,0.6)] focus-visible:ring-[#e8461e]/35',
line: 'border-current bg-transparent text-[#141311] hover:border-[#141311] hover:bg-[#141311] hover:text-[#f1ede4] focus-visible:ring-[#141311]/25',
}
export default function MagneticButton({ items, strength = 0.42, reach = 72, className = '' }) {
const root = useRef(null)
const { contextSafe } = useGSAP(
() => {
const el = root.current
// Layar sentuh: tidak ada listener, tidak ada transform. Tap langsung ke onClick.
if (!el || !window.matchMedia(FINE_POINTER).matches) return
const zones = gsap.utils.toArray('.mgb-zone', el)
const buttons = gsap.utils.toArray('.mgb-btn', el)
const labels = gsap.utils.toArray('.mgb-label', el)
// Zona pembungkus tidak ikut bergerak, jadi kotaknya = posisi diam tombol (koordinat layar).
let boxes = []
const measure = () => {
boxes = zones.map((z) => {
const b = z.getBoundingClientRect()
return { cx: b.left + b.width / 2, cy: b.top + b.height / 2, w: b.width, h: b.height }
})
}
measure()
const pointer = { x: 0, y: 0, active: false }
const bodies = buttons.map(() => ({ x: 0, y: 0, vx: 0, vy: 0, lx: 0, ly: 0, lvx: 0, lvy: 0, s: 1, vs: 0 }))
const setBtn = buttons.map((b) => ({ x: gsap.quickSetter(b, 'x', 'px'), y: gsap.quickSetter(b, 'y', 'px'), s: gsap.quickSetter(b, 'scale') }))
const setLbl = labels.map((l) => ({ x: gsap.quickSetter(l, 'x', 'px'), y: gsap.quickSetter(l, 'y', 'px') }))
// Fisika pegas di ticker GSAP: tertarik ke kursor, kembali saat kursor menjauh.
const tick = (_t, deltaMs) => {
const dt = Math.min(deltaMs, 40) / 1000
// Hanya tombol terdekat dalam jangkauan yang ditarik, supaya tombol bersebelahan tidak bertabrakan.
let nearest = -1
let nearestD = reach
const dist = boxes.map((box) => {
if (!pointer.active) return Infinity
const ex = Math.max(0, Math.abs(pointer.x - box.cx) - box.w / 2)
const ey = Math.max(0, Math.abs(pointer.y - box.cy) - box.h / 2)
return Math.hypot(ex, ey)
})
dist.forEach((d, i) => {
if (d < nearestD) {
nearestD = d
nearest = i
}
})
bodies.forEach((b, i) => {
let tx = 0
let ty = 0
let hot = 0
if (i === nearest) {
const box = boxes[i]
const f = 1 - dist[i] / reach
hot = f * f * (3 - 2 * f)
tx = gsap.utils.clamp(-22, 22, (pointer.x - box.cx) * strength * hot)
ty = gsap.utils.clamp(-14, 14, (pointer.y - box.cy) * strength * hot)
}
// Diam dan tidak ada tarikan: lewati, tidak perlu menulis transform tiap frame.
if (!hot && Math.abs(b.x) + Math.abs(b.y) + Math.abs(b.vx) + Math.abs(b.vy) + Math.abs(b.s - 1) + Math.abs(b.vs) < 0.01) return
// Tombol: pegas yang sedikit lewat saat dilepas (redaman 13, aslinya 11, supaya lebih tenang).
b.vx += (200 * (tx - b.x) - 13 * b.vx) * dt
b.vy += (200 * (ty - b.y) - 13 * b.vy) * dt
b.x += b.vx * dt
b.y += b.vy * dt
// Label bergerak sedikit lebih jauh dari tombolnya (paralaks).
b.lvx += (260 * (tx * 0.32 - b.lx) - 13 * b.lvx) * dt
b.lvy += (260 * (ty * 0.32 - b.ly) - 13 * b.lvy) * dt
b.lx += b.lvx * dt
b.ly += b.lvy * dt
b.vs += (300 * (1 + hot * 0.06 - b.s) - 16 * b.vs) * dt
b.s += b.vs * dt
setBtn[i].x(b.x)
setBtn[i].y(b.y)
setBtn[i].s(b.s)
setLbl[i].x(b.lx)
setLbl[i].y(b.ly)
})
}
gsap.ticker.add(tick)
// Kursor dibaca di seluruh jendela: tarikan mulai `reach` px sebelum kursor menyentuh tombol.
const onMove = (e) => {
if (e.pointerType !== 'mouse') return
measure()
pointer.x = e.clientX
pointer.y = e.clientY
pointer.active = true
}
const onOut = (e) => {
if (!e.relatedTarget) pointer.active = false
}
const onDown = (e) => {
if (e.pointerType !== 'mouse') return
const i = buttons.findIndex((b) => b.contains(e.target))
if (i >= 0) bodies[i].vs -= 2.2
}
window.addEventListener('pointermove', onMove, { passive: true })
window.addEventListener('scroll', measure, { passive: true })
window.addEventListener('resize', measure)
document.addEventListener('pointerout', onOut)
el.addEventListener('pointerdown', onDown)
return () => {
gsap.ticker.remove(tick)
window.removeEventListener('pointermove', onMove)
window.removeEventListener('scroll', measure)
window.removeEventListener('resize', measure)
document.removeEventListener('pointerout', onOut)
el.removeEventListener('pointerdown', onDown)
}
},
{ scope: root, dependencies: [strength, reach, items.length], revertOnUpdate: true },
)
// Perilaku klik lama dipanggil dulu, baru label "ditekan" sebentar.
const click = (i, e) => {
items[i]?.onClick?.(e)
const label = e.currentTarget.querySelector('.mgb-label')
contextSafe(() => {
gsap.fromTo(label, { scale: 0.88 }, { scale: 1, duration: 0.75, ease: 'power3.out' })
})()
}
return (
<div ref={root} className={`flex flex-wrap items-center gap-4 ${className}`}>
{items.map((item, i) => (
<span key={i} className="mgb-zone relative inline-flex">
<a
href={item.href}
onClick={(e) => click(i, e)}
className={`mgb-btn group relative inline-flex cursor-pointer items-center rounded-full border-[1.5px] border-solid px-[22px] py-[14px] text-[16px] font-medium whitespace-nowrap no-underline transition-[background-color,color,border-color,box-shadow] duration-300 will-change-transform focus-visible:ring-4 focus-visible:outline-none ${VARIANT_CLASS[item.variant || 'accent']}`}
>
<span className="mgb-label inline-flex items-center gap-2.5">
{item.label}
{item.arrow && <span aria-hidden="true" className="transition-transform duration-300 group-hover:translate-x-1">→</span>}
</span>
</a>
</span>
))}
</div>
)
}
Langkah 6 · Bandingkan
Di layar lebar, kartu kanan hidup: dekatkan kursor ke tombol Ceritakan usahamu. Di HP kartu menampilkan rekamannya, karena layar sentuh tidak punya kursor. Tombol di web aslinya tetap bisa ditekan di HP, hanya tanpa tarikan magnet.
Mau detail kecil seperti ini di web-mu tanpa memasangnya sendiri? Tim InWebsite bisa bikinin web-mu.
Lihat paket InWebsite →