{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"glass-lens","type":"registry:component","title":"Glass lens","description":"A ball of glass rides over the words and magnifies whatever it passes; press and the whole paragraph shivers and flushes indigo.","author":"Yash Bavadiya <https://xevrion.dev>","dependencies":["motion"],"registryDependencies":["utils"],"files":[{"path":"components/glass-lens.tsx","type":"registry:component","content":"\"use client\";\n\nimport { useEffect, useId, useLayoutEffect, useRef, useState } from \"react\";\nimport {\n  animate,\n  motion,\n  useMotionTemplate,\n  useMotionValue,\n  useSpring,\n  useTransform,\n  type MotionValue,\n} from \"motion/react\";\nimport { usePreviewPlay } from \"@/lib/preview-play\";\nimport { useReducedMotion } from \"@/hooks/use-reduced-motion\";\nimport { cn } from \"@/lib/utils\";\n\n// After Gionatannese's GN.D portfolio (https://x.com/gionatannese): a ball of\n// glass and chrome that rides over the type and magnifies whatever is under\n// it. Press, and the whole paragraph shivers and flushes indigo for a\n// moment. Left alone, the lens drifts across the words by itself.\n\n// The lens's radius and how much it enlarges.\nconst R = 66;\nconst MAG = 1.75;\n// The lens trails the pointer a touch, like something with a little\n// weight.\nconst FOLLOW = { stiffness: 520, damping: 42, mass: 0.6 };\n\n// A press: up fast, then settling out slowly, like a struck surface.\nfunction strike(shiver: MotionValue<number>, reduceMotion: boolean) {\n  if (reduceMotion) {\n    animate(shiver, [0, 0.4, 0], { duration: 0.6 });\n    return;\n  }\n  animate(shiver, [0, 1, 0], {\n    duration: 0.95,\n    times: [0, 0.18, 1],\n    ease: [\"easeOut\", [0.22, 1, 0.36, 1]],\n  });\n}\n\nexport function GlassLens({\n  text = \"Turning bold ideas into ambitious and meticulously crafted things that actually stick.\",\n  className,\n}: {\n  text?: string;\n  className?: string;\n}) {\n  const reduceMotion = useReducedMotion();\n  const play = usePreviewPlay();\n  const warpId = useId().replace(/:/g, \"\");\n  const box = useRef<HTMLDivElement>(null);\n  const turbulence = useRef<SVGFETurbulenceElement>(null);\n  const displace = useRef<SVGFEDisplacementMapElement>(null);\n  const [width, setWidth] = useState(0);\n  const pointerIn = useRef(false);\n\n  // Where the lens is headed, and where it is.\n  const tx = useMotionValue(140);\n  const ty = useMotionValue(70);\n  const sx = useSpring(tx, FOLLOW);\n  const sy = useSpring(ty, FOLLOW);\n  const x = reduceMotion ? tx : sx;\n  const y = reduceMotion ? ty : sy;\n\n  // The magnified copy is the same paragraph, moved so the spot under the\n  // lens sits in its middle, then enlarged about that spot.\n  const left = useTransform(x, (v) => v - R);\n  const top = useTransform(y, (v) => v - R);\n  const innerX = useTransform(x, (v) => R - v);\n  const innerY = useTransform(y, (v) => R - v);\n  const origin = useMotionTemplate`${x}px ${y}px`;\n  const shadowTop = useTransform(top, (v) => v + 16);\n\n  // 0 at rest, 1 at the height of a press.\n  const shiver = useMotionValue(0);\n  const tint = useTransform(shiver, [0, 0.35, 1], [0, 1, 1]);\n\n  useLayoutEffect(() => {\n    const el = box.current;\n    if (!el) return;\n    const measure = () => setWidth(el.offsetWidth);\n    measure();\n    const ro = new ResizeObserver(measure);\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, []);\n\n  // The shiver drives the warp filter directly, never React.\n  useEffect(\n    () =>\n      shiver.on(\"change\", (v) => {\n        displace.current?.setAttribute(\"scale\", String(v * 16));\n        turbulence.current?.setAttribute(\"seed\", String(Math.round(v * 40)));\n      }),\n    [shiver],\n  );\n\n  // Drifts over the words while nobody is pointing at it: on a phone, in an\n  // index card, or after the pointer leaves.\n  useEffect(() => {\n    const el = box.current;\n    if (!el || reduceMotion || play === false) return;\n    let frame = 0;\n    let visible = false;\n    const start = performance.now();\n    const loop = (now: number) => {\n      if (!pointerIn.current) {\n        const t = (now - start) / 1000;\n        const w = el.offsetWidth;\n        const h = el.offsetHeight;\n        // Swings across the words but stays over them, so on a narrow\n        // phone it never hangs off the edge or magnifies the margin.\n        // Balanced lines stop short of the edge, so it swings a little\n        // less than the full width.\n        const reachX = Math.max(0, w / 2 - R * 0.9) * 0.72;\n        const reachY = Math.max(0, h / 2 - R * 0.7);\n        tx.set(w / 2 + reachX * Math.sin(t * 0.42));\n        ty.set(h / 2 + reachY * Math.sin(t * 0.67 + 1.2));\n      }\n      frame = visible ? requestAnimationFrame(loop) : 0;\n    };\n    const io = new IntersectionObserver(([entry]) => {\n      visible = entry.isIntersecting;\n      if (visible && !frame) frame = requestAnimationFrame(loop);\n    });\n    io.observe(el);\n    return () => {\n      cancelAnimationFrame(frame);\n      io.disconnect();\n    };\n  }, [play, reduceMotion, tx, ty]);\n\n  // The card's show: a press every few seconds.\n  useEffect(() => {\n    if (!play || reduceMotion) return;\n    const timer = setInterval(() => strike(shiver, false), 2600);\n    return () => clearInterval(timer);\n  }, [play, reduceMotion, shiver]);\n\n  const press = () => strike(shiver, reduceMotion);\n\n  const local = (e: React.PointerEvent) => {\n    const rect = e.currentTarget.getBoundingClientRect();\n    // Layout size over drawn size, in case it's shown scaled.\n    const k = e.currentTarget.clientWidth / rect.width;\n    return { px: (e.clientX - rect.left) * k, py: (e.clientY - rect.top) * k };\n  };\n\n  const paragraph = (\n    <p className=\"font-serif text-[clamp(28px,7vw,42px)] leading-[1.12] tracking-[-0.015em] text-balance\">\n      {text}\n    </p>\n  );\n\n  return (\n    <div\n      ref={box}\n      className={cn(\n        \"relative w-[560px] max-w-full touch-none select-none [@media(pointer:fine)]:cursor-none\",\n        className,\n      )}\n      onPointerMove={(e) => {\n        if (play !== null) return;\n        // A finger moves the lens only while it's down, so a scroll that\n        // passes over still scrolls.\n        if (e.pointerType === \"touch\" && e.buttons === 0) return;\n        const { px, py } = local(e);\n        pointerIn.current = true;\n        tx.set(px);\n        ty.set(py);\n      }}\n      onPointerDown={(e) => {\n        if (play !== null) return;\n        const { px, py } = local(e);\n        pointerIn.current = true;\n        tx.set(px);\n        ty.set(py);\n        press();\n      }}\n      onPointerLeave={() => {\n        pointerIn.current = false;\n      }}\n      onPointerUp={(e) => {\n        if (e.pointerType === \"touch\") pointerIn.current = false;\n      }}\n    >\n      <svg aria-hidden className=\"absolute size-0\">\n        <filter id={warpId} x=\"-10%\" y=\"-20%\" width=\"120%\" height=\"140%\">\n          <feTurbulence\n            ref={turbulence}\n            type=\"fractalNoise\"\n            baseFrequency=\"0.012 0.045\"\n            numOctaves={2}\n            seed={0}\n          />\n          <feDisplacementMap\n            ref={displace}\n            in=\"SourceGraphic\"\n            scale={0}\n            xChannelSelector=\"R\"\n            yChannelSelector=\"G\"\n          />\n        </filter>\n      </svg>\n\n      {/* The words, and an indigo copy laid over them that shows during a\n          press, both warped together. */}\n      <div className=\"relative py-6\" style={{ filter: `url(#${warpId})` }}>\n        <div className=\"text-foreground\">{paragraph}</div>\n        <motion.div\n          aria-hidden\n          className=\"absolute inset-0 py-6 text-[light-dark(oklch(0.45_0.25_272),oklch(0.72_0.17_272))]\"\n          style={{ opacity: tint }}\n        >\n          {paragraph}\n        </motion.div>\n      </div>\n\n      {/* The lens. */}\n      <motion.div\n        aria-hidden\n        className=\"pointer-events-none absolute top-0 left-0 overflow-hidden rounded-full bg-background\"\n        style={{ x: left, y: top, width: R * 2, height: R * 2 }}\n      >\n        <motion.div\n          className=\"absolute top-0 left-0 py-6\"\n          style={{\n            x: innerX,\n            y: innerY,\n            width: width || undefined,\n            scale: MAG,\n            transformOrigin: origin,\n            filter: `url(#${warpId})`,\n          }}\n        >\n          <div className=\"text-foreground\">{paragraph}</div>\n          <motion.div\n            className=\"absolute inset-0 py-6 text-[light-dark(oklch(0.45_0.25_272),oklch(0.72_0.17_272))]\"\n            style={{ opacity: tint }}\n          >\n            {paragraph}\n          </motion.div>\n        </motion.div>\n\n        {/* The glass: darker towards the rim where it bends the most, a\n            cool indigo reflection pooled at the bottom, a bright window\n            caught at the top left, and a thin chrome edge. */}\n        <span className=\"absolute inset-0 rounded-full bg-[radial-gradient(circle_at_50%_50%,transparent_58%,rgb(0_0_0/0.18)_80%,rgb(0_0_0/0.55)_100%)]\" />\n        <span className=\"absolute inset-0 rounded-full bg-[radial-gradient(120%_70%_at_50%_118%,oklch(0.5_0.24_272/0.55),transparent_60%)] mix-blend-multiply dark:mix-blend-screen\" />\n        <span className=\"absolute top-[11%] left-[19%] h-[22%] w-[34%] -rotate-[24deg] rounded-[50%] bg-[radial-gradient(closest-side,rgb(255_255_255/0.95),rgb(255_255_255/0)_100%)]\" />\n        <span className=\"absolute top-[30%] left-[14%] size-[7%] rounded-full bg-white/80 blur-[0.5px]\" />\n        <span className=\"absolute inset-0 rounded-full shadow-[inset_0_0_0_1.5px_rgb(255_255_255/0.55),inset_0_0_0_2.5px_rgb(0_0_0/0.35),inset_0_10px_18px_-6px_rgb(255_255_255/0.6),inset_0_-14px_22px_-8px_rgb(0_0_0/0.45)]\" />\n      </motion.div>\n\n      {/* Its shadow on the page, offset down as if lit from above. */}\n      <motion.div\n        aria-hidden\n        className=\"pointer-events-none absolute top-0 left-0 -z-10 rounded-full bg-black/25 blur-xl dark:bg-black/60\"\n        style={{ x: left, y: shadowTop, width: R * 2, height: R * 2 }}\n      />\n    </div>\n  );\n}\n\nexport default function GlassLensDemo() {\n  return <GlassLens />;\n}\n"},{"path":"lib/preview-play.ts","type":"registry:lib","target":"lib/preview-play.ts","content":"\"use client\";\n\nimport { createContext, useContext } from \"react\";\n\n// Lets a demo know whether it's showing in an index card, and whether that\n// card is hovered or focused. Demos use it to act themselves out on hover:\n// a star button stars itself, a checkbox scribbles, and so on.\n//\n// null: not in a preview (the component's own page, or real use).\n// false: in a preview, at rest.\n// true: in a preview, being hovered or focused: play the show.\nexport const PreviewPlayContext = createContext<boolean | null>(null);\n\nexport function usePreviewPlay() {\n  return useContext(PreviewPlayContext);\n}\n"},{"path":"hooks/use-reduced-motion.ts","type":"registry:hook","target":"hooks/use-reduced-motion.ts","content":"\"use client\";\n\nimport { useSyncExternalStore } from \"react\";\n\n// Motion's useReducedMotion reads the media query on the very first client\n// render, but the server can't know it, so every component that renders\n// differently under reduced motion broke hydration for those users. This\n// reports false during hydration, matching the server HTML, then the real\n// preference straight after, and follows it if it changes.\nconst query = \"(prefers-reduced-motion: reduce)\";\n\nfunction subscribe(onChange: () => void) {\n  const media = window.matchMedia(query);\n  media.addEventListener(\"change\", onChange);\n  return () => media.removeEventListener(\"change\", onChange);\n}\n\nexport function useReducedMotion() {\n  return useSyncExternalStore(\n    subscribe,\n    () => window.matchMedia(query).matches,\n    () => false,\n  );\n}\n"}],"categories":["text"],"docs":"From ui lab: https://lab.xevrion.dev/lab/glass-lens"}