{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "icon-pagination", "type": "registry:ui", "title": "Icon Pagination", "description": "Colorful dot pagination with mouse proximity wave effect, cosine falloff lift, and spring physics.", "dependencies": [ "motion" ], "registryDependencies": [], "files": [ { "path": "registry/ruixenui/icon-pagination.tsx", "content": "\"use client\";\n\nimport { useState, useRef, useCallback } from \"react\";\nimport { motion } from \"motion/react\";\n\n/**\n * Icon Pagination — Rauno Freiberg craft.\n *\n * Row of colored dot indicators. Mouse proximity triggers wave\n * effect — nearby dots lift and brighten with cosine falloff.\n * Active dot has accent ring and elevated shadow.\n * Click to navigate. Audio tick on page change.\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 IconPaginationProps {\n totalPages?: number;\n maxVisible?: number;\n onChange?: (page: number) => void;\n sound?: boolean;\n}\n\n/* ── CSS ── */\n\nconst CSS = `.ip{--ip-bg:rgba(255,255,255,.72);--ip-border:rgba(0,0,0,.06);--ip-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);--ip-ink:0,0,0}.dark .ip,[data-theme=\"dark\"] .ip{--ip-bg:rgba(30,30,32,.82);--ip-border:rgba(255,255,255,.06);--ip-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);--ip-ink:255,255,255}`;\n\n/* ── Constants ── */\n\nconst COLORS = [\n \"#FF6B6B\",\n \"#51CF66\",\n \"#339AF0\",\n \"#FCC419\",\n \"#CC5DE8\",\n \"#FF922B\",\n \"#20C997\",\n \"#F06595\",\n];\nconst PROX_RADIUS = 60;\nconst DOT_SIZE = 12;\nconst DOT_GAP = 10;\nconst SPRING = { type: \"spring\" as const, stiffness: 500, damping: 30 };\n\n/* ── Component ── */\n\nexport function IconPagination({\n totalPages = 8,\n maxVisible = 9,\n onChange,\n sound = true,\n}: IconPaginationProps) {\n const [active, setActive] = useState(0);\n const [hoverIdx, setHoverIdx] = useState(-1);\n const lastSound = useRef(0);\n const containerRef = useRef(null);\n\n const go = useCallback(\n (next: number) => {\n if (next < 0 || next >= totalPages || next === active) return;\n if (sound) tick(lastSound);\n setActive(next);\n onChange?.(next);\n },\n [active, totalPages, onChange, sound],\n );\n\n const count = Math.min(totalPages, maxVisible);\n\n // Visible window centered on active\n const half = Math.floor(count / 2);\n let start = active - half;\n let end = active + (count - half - 1);\n if (start < 0) {\n end += -start;\n start = 0;\n }\n if (end >= totalPages) {\n start -= end - totalPages + 1;\n end = totalPages - 1;\n start = Math.max(0, start);\n }\n\n const visible: number[] = [];\n for (let i = start; i <= end; i++) visible.push(i);\n\n const onMouseMove = useCallback(\n (e: React.MouseEvent) => {\n const rect = containerRef.current?.getBoundingClientRect();\n if (!rect) return;\n const x = e.clientX - rect.left;\n const dotW = DOT_SIZE + DOT_GAP;\n const padL = 34; // arrow + padding\n const idx = Math.round((x - padL - DOT_SIZE / 2) / dotW);\n setHoverIdx(idx >= 0 && idx < visible.length ? idx : -1);\n },\n [visible.length],\n );\n\n return (\n \n