{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "scroll-pagination", "type": "registry:ui", "title": "Scroll Pagination", "description": "Minimal flip-counter pagination with direction-aware vertical flip, progress bar, and spring physics.", "dependencies": [ "motion" ], "registryDependencies": [], "files": [ { "path": "registry/ruixenui/scroll-pagination.tsx", "content": "\"use client\";\n\nimport { useState, useRef, useCallback, useEffect } from \"react\";\nimport { motion, AnimatePresence } from \"motion/react\";\n\n/**\n * Scroll Pagination — Rauno Freiberg craft.\n *\n * Minimal flip-counter pagination. Single page number displayed\n * at center with direction-aware vertical flip on change.\n * Glass pill container. Arrow buttons with spring hover.\n * Scroll wheel or keyboard to navigate. Audio tick on every flip.\n */\n\n/* ── Audio singleton ── */\n\nlet _a: AudioContext | null = null;\nlet _b: AudioBuffer | null = null;\n\nfunction getCtx(): AudioContext {\n if (!_a)\n _a = new (window.AudioContext ||\n (window as unknown as { webkitAudioContext: typeof AudioContext })\n .webkitAudioContext)();\n if (_a.state === \"suspended\") _a.resume();\n return _a;\n}\n\nfunction getBuf(ac: AudioContext): AudioBuffer {\n if (_b && _b.sampleRate === ac.sampleRate) return _b;\n const len = Math.floor(ac.sampleRate * 0.003);\n const buf = ac.createBuffer(1, len, ac.sampleRate);\n const ch = buf.getChannelData(0);\n for (let i = 0; i < len; i++)\n ch[i] = (Math.random() * 2 - 1) * (1 - i / len) ** 4;\n _b = buf;\n return buf;\n}\n\nfunction tick(ref: React.MutableRefObject) {\n const now = performance.now();\n if (now - ref.current < 25) return;\n ref.current = now;\n try {\n const ac = getCtx();\n const src = ac.createBufferSource();\n const g = ac.createGain();\n src.buffer = getBuf(ac);\n g.gain.value = 0.06;\n src.connect(g).connect(ac.destination);\n src.start();\n } catch {\n /* silent */\n }\n}\n\n/* ── Types ── */\n\ninterface ScrollPaginationProps {\n totalPages?: number;\n onChange?: (page: number) => void;\n sound?: boolean;\n}\n\n/* ── CSS ── */\n\nconst CSS = `.sp{--sp-bg:rgba(255,255,255,.72);--sp-border:rgba(0,0,0,.06);--sp-shadow:0 0 0 .5px rgba(0,0,0,.04),0 2px 4px rgba(0,0,0,.04),0 8px 24px rgba(0,0,0,.06);--sp-ink:0,0,0}.dark .sp,[data-theme=\"dark\"] .sp{--sp-bg:rgba(30,30,32,.82);--sp-border:rgba(255,255,255,.06);--sp-shadow:0 0 0 .5px rgba(255,255,255,.04),0 2px 4px rgba(0,0,0,.2),0 8px 24px rgba(0,0,0,.3);--sp-ink:255,255,255}.sp-btn{cursor:pointer;border:none;background:none;display:flex;align-items:center;justify-content:center;padding:8px;border-radius:8px;transition:background .12s}.sp-btn:hover{background:rgba(var(--sp-ink),.05)}.sp-btn:active{background:rgba(var(--sp-ink),.08)}`;\n\n/* ── Constants ── */\n\nconst SPRING = { type: \"spring\" as const, stiffness: 500, damping: 35 };\n\nfunction clamp(v: number, lo: number, hi: number) {\n return Math.max(lo, Math.min(hi, v));\n}\n\n/* ── Component ── */\n\nexport function ScrollPagination({\n totalPages = 20,\n onChange,\n sound = true,\n}: ScrollPaginationProps) {\n const [active, setActive] = useState(0);\n const [dir, setDir] = useState(1);\n const lastSound = useRef(0);\n const scrollAccum = useRef(0);\n const containerRef = useRef(null);\n\n const go = useCallback(\n (next: number) => {\n const c = clamp(next, 0, totalPages - 1);\n if (c === active) return;\n setDir(c > active ? 1 : -1);\n if (sound) tick(lastSound);\n setActive(c);\n onChange?.(c);\n },\n [active, totalPages, onChange, sound],\n );\n\n // Wheel\n useEffect(() => {\n const el = containerRef.current;\n if (!el) return;\n const handler = (e: WheelEvent) => {\n e.preventDefault();\n scrollAccum.current += e.deltaY;\n if (Math.abs(scrollAccum.current) >= 40) {\n const d = Math.sign(scrollAccum.current);\n scrollAccum.current = 0;\n go(active + d);\n }\n };\n el.addEventListener(\"wheel\", handler, { passive: false });\n return () => el.removeEventListener(\"wheel\", handler);\n }, [go, active]);\n\n const progress = totalPages > 1 ? active / (totalPages - 1) : 0;\n\n return (\n \n