{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "wordmark-footer", "type": "registry:ui", "title": "Wordmark Footer", "description": "A Rauno-craft half-cut wordmark footer — giant brand text on a full-width dark surface with luminance gradient and hairline divider.", "dependencies": [ "motion" ], "files": [ { "path": "registry/ruixenui/wordmark-footer.tsx", "content": "\"use client\";\n\nimport { useState, useRef, useEffect, useCallback } from \"react\";\nimport { motion } from \"motion/react\";\n\n/**\n * Wordmark Footer — Rauno Freiberg craft, Ruixen UI edition.\n *\n * Centered giant wordmark on a full-width dark surface with a subtle\n * bottom clip. Move your cursor and the metallic shine follows —\n * a radial gradient spotlight tracks the pointer with lerped\n * smoothing at 60fps via direct DOM writes, zero re-renders.\n * A vertical mask handles the half-cut fade independently.\n * cursor: pointer. The light IS the interaction.\n */\n\n/* ── Types ── */\n\ninterface WordmarkFooterProps {\n brandName?: string;\n}\n\n/* ── Scoped CSS ── */\n\nconst STYLE = `\n.wf{\n --wf-bg:#161618;\n --wf-line:rgba(255,255,255,.07)\n}\n.dark .wf,[data-theme=\"dark\"] .wf{\n --wf-bg:#111113;\n --wf-line:rgba(255,255,255,.08)\n}\n`.replace(/\\n/g, \"\");\n\n/* ── Radial shine gradient — follows cursor ── */\n\nfunction makeShine(x: number, y: number): string {\n return `radial-gradient(ellipse 100% 100% at ${x.toFixed(1)}% ${y.toFixed(1)}%, rgba(255,255,255,.88) 0%, rgba(255,255,255,.62) 24%, rgba(255,255,255,.34) 50%, rgba(255,255,255,.16) 100%)`;\n}\n\n/* ── Vertical mask — dims bottom for the half-cut, independent of shine ── */\n\nconst VMASK =\n \"linear-gradient(to bottom, black 0%, black 38%, rgba(0,0,0,.55) 76%, rgba(0,0,0,.30) 100%)\";\n\n/* ── Main component ── */\n\nexport function WordmarkFooter({\n brandName = \"Ruixen UI\",\n}: WordmarkFooterProps) {\n const [inView, setInView] = useState(false);\n const sectionRef = useRef(null);\n const textRef = useRef(null);\n\n /* ── Cursor-tracking state — refs only, zero re-renders ── */\n\n const hovering = useRef(false);\n const curX = useRef(50);\n const curY = useRef(30);\n const tgtX = useRef(50);\n const tgtY = useRef(30);\n const raf = useRef(0);\n\n /* ── Per-frame lerp loop — direct DOM writes ── */\n\n const paint = useCallback(() => {\n curX.current += (tgtX.current - curX.current) * 0.1;\n curY.current += (tgtY.current - curY.current) * 0.1;\n\n const grad = makeShine(curX.current, curY.current);\n\n if (textRef.current) {\n textRef.current.style.backgroundImage = grad;\n }\n\n const dx = Math.abs(tgtX.current - curX.current);\n const dy = Math.abs(tgtY.current - curY.current);\n\n if (hovering.current || dx > 0.05 || dy > 0.05) {\n raf.current = requestAnimationFrame(paint);\n }\n }, []);\n\n const onMove = useCallback(\n (e: React.MouseEvent) => {\n const r = sectionRef.current?.getBoundingClientRect();\n if (!r) return;\n tgtX.current = ((e.clientX - r.left) / r.width) * 100;\n tgtY.current = ((e.clientY - r.top) / r.height) * 100;\n\n if (!hovering.current) {\n hovering.current = true;\n raf.current = requestAnimationFrame(paint);\n }\n },\n [paint],\n );\n\n const onLeave = useCallback(() => {\n hovering.current = false;\n tgtX.current = 50;\n tgtY.current = 30;\n raf.current = requestAnimationFrame(paint);\n }, [paint]);\n\n /* ── Cleanup ── */\n\n useEffect(() => () => cancelAnimationFrame(raf.current), []);\n\n /* ── IntersectionObserver ── */\n\n useEffect(() => {\n const el = sectionRef.current;\n if (!el || typeof IntersectionObserver === \"undefined\") {\n setInView(true);\n return;\n }\n const obs = new IntersectionObserver(\n ([entry]) => {\n if (entry.isIntersecting) {\n setInView(true);\n obs.disconnect();\n }\n },\n { threshold: 0.1 },\n );\n obs.observe(el);\n return () => obs.disconnect();\n }, []);\n\n return (\n \n