{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"levitating-link-nav","type":"registry:component","title":"Levitating Link Nav","description":"A dark glass nav bar whose link labels hang in the air above a lit shelf, each over its own soft shadow; the link you point at, and the one for the section in view, lands.","dependencies":[],"registryDependencies":[],"files":[{"path":"components/viberdy/levitating-link-nav.tsx","content":"\"use client\";\n\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport type { CSSProperties, RefObject } from \"react\";\n\n/**\n * LevitatingLinkNav — a dark glass bar whose link labels hang in the air above a lit shelf.\n *\n * Each label floats a few pixels above the shelf over its own soft shadow,\n * and bobs on its own slow period (9.7 to 16.7 s), so the row never moves\n * in step. The shadow does the work: the higher a label, the wider, softer\n * and fainter its shadow. Point at a link or tab to it and it lands (a\n * critically damped spring, about 260 ms): it drops onto the shelf and its\n * shadow tightens into a contact line. With track on, the link whose\n * section is in view stays landed and is marked aria-current. Over the\n * first 120px of scroll the bar tightens and its glass fills in, read from\n * whatever scrolls it. Below a 768px container the links move into a sheet\n * that closes on Escape or an outside tap. Reduced motion holds the labels\n * still; the loop runs only while something moves and the bar is on screen.\n *\n * Part of the Attention kit: a style of anomalies, familiar things made wrong\n * (fish in dry air, a stone that breaks upward, a shadow that runs toward its\n * lamp). One subject, one motivated light, one broken rule, and everything\n * else calm. Cut byte for byte from the kit's one site (AttentionSite, a\n * demo studio called Aerium). Paste it as its own file: it carries its own\n * copy of every helper it uses and has no dependencies.\n */\n\n/**\n * The scrolling box an element lives in: the nearest ancestor that actually\n * scrolls, or null for the window. Resolved when asked, because a box only\n * becomes a scroller once its content is taller than it.\n */\nfunction aScrollParent(el: Element | null): HTMLElement | null {\n  let n = el?.parentElement ?? null;\n  while (n && n !== document.body && n !== document.documentElement) {\n    const oy = getComputedStyle(n).overflowY;\n    if ((oy === \"auto\" || oy === \"scroll\" || oy === \"overlay\") && n.scrollHeight > n.clientHeight + 1) return n;\n    n = n.parentElement;\n  }\n  return null;\n}\n\ninterface AFrame {\n  /** The element's box, in viewport pixels. */\n  rect: DOMRect;\n  /** The visible part of whatever scrolls it: the scroller's box, or the window. */\n  view: { top: number; height: number; left: number; width: number };\n}\n\n/**\n * Run `apply` with the element's position whenever anything scrolls or\n * resizes, at most once per frame. One capture-phase scroll listener hears\n * every scroller, so the same piece works on a page and inside a scrolling\n * preview. `apply` writes to the DOM; nothing here re-renders React.\n */\nfunction useAScrollFrame<T extends HTMLElement>(ref: RefObject<T | null>, apply: (f: AFrame) => void) {\n  const applyRef = useRef(apply);\n  useEffect(() => {\n    applyRef.current = apply;\n  });\n  useEffect(() => {\n    let raf = 0;\n    const run = () => {\n      raf = 0;\n      const el = ref.current;\n      if (!el) return;\n      const sp = aScrollParent(el);\n      const r = sp ? sp.getBoundingClientRect() : null;\n      const view = sp && r ? { top: r.top, height: sp.clientHeight, left: r.left, width: sp.clientWidth } : { top: 0, height: window.innerHeight, left: 0, width: window.innerWidth };\n      applyRef.current({ rect: el.getBoundingClientRect(), view });\n    };\n    const kick = () => {\n      if (!raf) raf = requestAnimationFrame(run);\n    };\n    document.addEventListener(\"scroll\", kick, { capture: true, passive: true });\n    window.addEventListener(\"resize\", kick);\n    const ro = typeof ResizeObserver !== \"undefined\" ? new ResizeObserver(kick) : null;\n    if (ref.current && ro) ro.observe(ref.current);\n    run();\n    return () => {\n      document.removeEventListener(\"scroll\", kick, { capture: true });\n      window.removeEventListener(\"resize\", kick);\n      ro?.disconnect();\n      if (raf) cancelAnimationFrame(raf);\n    };\n  }, [ref]);\n}\n\n/** 0..1, clamped. */\nconst aClamp = (v: number) => (v < 0 ? 0 : v > 1 ? 1 : v);\n\n/** The user's reduced-motion setting, live. Server and first render say \"no\". */\nfunction useAReducedMotion(): boolean {\n  const [reduced, setReduced] = useState(false);\n  useEffect(() => {\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    const on = () => setReduced(mq.matches);\n    on();\n    mq.addEventListener(\"change\", on);\n    return () => mq.removeEventListener(\"change\", on);\n  }, []);\n  return reduced;\n}\n\n/** Links built from props: https, http, mailto, same-site paths and anchors only (tabs and newlines stripped, backslashes refused). */\nfunction aSafeHref(href: string): string {\n  const h = href.replace(/[\\t\\n\\r]/g, \"\").trim();\n  if (!h || h.includes(String.fromCharCode(92))) return \"#\";\n  if (h.startsWith(\"#\") || (h.startsWith(\"/\") && !h.startsWith(\"//\"))) return h;\n  return /^(https?:|mailto:)/i.test(h) ? h : \"#\";\n}\n\n/** Find a same-page anchor target: [data-a-anchor=\"id\"] inside the same site, else an element with that id. */\nfunction aAnchor(from: Element, id: string): HTMLElement | null {\n  const root = from.closest(\"[data-a-site]\") ?? document;\n  return root.querySelector<HTMLElement>('[data-a-anchor=\"' + id + '\"]') ?? document.getElementById(id);\n}\n\n/** Glide whatever scrolls the target to a same-page anchor; anything else is left to the browser. */\nfunction aGo(e: { currentTarget: HTMLAnchorElement; preventDefault: () => void }) {\n  const href = e.currentTarget.getAttribute(\"href\") ?? \"\";\n  if (!href.startsWith(\"#\") || href.length < 2) return;\n  const target = aAnchor(e.currentTarget, href.slice(1));\n  if (!target) return;\n  e.preventDefault();\n  const sp = aScrollParent(target);\n  const behavior: ScrollBehavior = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches ? \"auto\" : \"smooth\";\n  if (sp) sp.scrollTo({ top: sp.scrollTop + target.getBoundingClientRect().top - sp.getBoundingClientRect().top, behavior });\n  else window.scrollTo({ top: window.scrollY + target.getBoundingClientRect().top, behavior });\n}\n\n/** The kit's type: Instrument Sans (through --font-instrument) over Geist. */\nconst A_SANS = 'var(--font-instrument, \"Instrument Sans\"), \"Geist\", system-ui, sans-serif';\n\n/** A link in the kit's nav, footer and plans: label and href (through aSafeHref). */\ntype ALink = { label: string; href: string };\n\nconst A_NAV_LINKS: ALink[] = [\n  { label: \"Studio\", href: \"#studio\" },\n  { label: \"Rooms\", href: \"#hall\" },\n  { label: \"Visit\", href: \"#visit\" },\n  { label: \"Plans\", href: \"#plans\" },\n];\n\n/** Each label bobs on its own period (seconds, primes-ish) so the row never moves in step. */\nconst A_PERIODS = [9.7, 11.3, 12.7, 13.9, 15.1, 16.7];\n\n/** The kit's mark: a dot that hangs over a line, with its shadow on the line. */\nfunction ALevMark({ size = 16 }: { size?: number }) {\n  return (\n    <svg width={size} height={size} viewBox=\"0 0 16 16\" aria-hidden=\"true\" className=\"shrink-0\">\n      <circle data-a-dot=\"\" cx=\"8\" cy=\"5.2\" r=\"3\" fill=\"currentColor\" />\n      <ellipse data-a-dotshadow=\"\" cx=\"8\" cy=\"12.4\" rx=\"3.4\" ry=\"0.9\" fill=\"currentColor\" opacity=\"0.32\" />\n      <rect x=\"1.5\" y=\"13.4\" width=\"13\" height=\"1.2\" rx=\"0.6\" fill=\"currentColor\" />\n    </svg>\n  );\n}\n\n/** One floating link: the label, and its shadow on the shelf. Its height is written by the nav's loop. */\nfunction AFloatLink({ link, shelf, onGo }: { link: ALink; shelf?: boolean; onGo?: () => void }) {\n  const href = aSafeHref(link.href);\n  return (\n    <a\n      href={href}\n      data-a-float=\"\"\n      data-href={href}\n      onClick={(e) => {\n        aGo(e);\n        onGo?.();\n      }}\n      className=\"group relative block rounded-[2px] px-3 pt-2.5 pb-[11px] text-[14px] leading-none font-[450] tracking-[-0.01em] text-[#eaf1f6]/72 transition-[color] duration-200 hover:text-[#eaf1f6] focus-visible:text-[#eaf1f6] focus-visible:outline-2! focus-visible:outline-offset-[-2px]! focus-visible:outline-[#d9ecf8]! aria-[current]:text-[#eaf1f6]\"\n    >\n      {shelf && <span aria-hidden=\"true\" className=\"a-shelf pointer-events-none absolute inset-x-1 bottom-[5px] h-[5px]\" />}\n      <span data-a-glow=\"\" aria-hidden=\"true\" className=\"pointer-events-none absolute inset-x-1 bottom-[1px] h-[11px] opacity-0\" style={{ background: \"radial-gradient(closest-side, rgba(217,236,248,0.4), rgba(217,236,248,0))\" }} />\n      <span data-a-label=\"\" className=\"relative z-[1] inline-block will-change-transform\" style={{ transform: \"translate3d(0,-5px,0)\" }}>\n        {link.label}\n      </span>\n      <span\n        data-a-shadow=\"\"\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute bottom-[5px] left-1/2 h-[4px] w-[64%] rounded-[50%]\"\n        style={{ background: \"radial-gradient(closest-side, rgba(2,4,6,0.95), rgba(2,4,6,0))\", transform: \"translateX(-50%)\" }}\n      />\n    </a>\n  );\n}\n\nexport interface LevitatingLinkNavProps {\n  /** The wordmark. */\n  brand?: string;\n  /** Where the wordmark goes (through aSafeHref). */\n  homeHref?: string;\n  /** The links; \"#anchor\" hrefs glide to their section and, with `track`, land while it is in view. */\n  links?: ALink[];\n  /** The one button. null hides it. */\n  cta?: ALink | null;\n  /** Land the link whose section is in view ([data-a-anchor] in the same site, or an element with that id). */\n  track?: boolean;\n  /** How high the labels hang above the shelf, 2-10px. */\n  float?: number;\n  /** Let each label bob a little on its own slow period. Reduced motion always holds them still. */\n  bob?: boolean;\n  className?: string;\n  style?: CSSProperties;\n}\n\n/**\n * LevitatingLinkNav — a dark glass bar whose link labels hang in the air\n * above a lit shelf, each over its own shadow; the one you point at lands.\n */\nexport function LevitatingLinkNav({\n  brand = \"Aerium\",\n  homeHref = \"#top\",\n  links = A_NAV_LINKS,\n  cta = { label: \"Commission a room\", href: \"#commission\" },\n  track = true,\n  float = 5,\n  bob = true,\n  className = \"\",\n  style,\n}: LevitatingLinkNavProps) {\n  const rootRef = useRef<HTMLElement>(null);\n  const barRef = useRef<HTMLDivElement>(null);\n  const menuRef = useRef<HTMLButtonElement>(null);\n  const sheetRef = useRef<HTMLDivElement>(null);\n  const hoverEl = useRef<Element | null>(null);\n  const active = useRef<string | null>(null);\n  const kick = useRef<() => void>(() => {});\n  const [open, setOpen] = useState(false);\n  const sheetId = useId();\n  const reduced = useAReducedMotion();\n  const F = Math.min(10, Math.max(2, Number.isFinite(float) ? float : 5));\n  const opts = useRef({ F, bob, reduced });\n  useEffect(() => {\n    opts.current = { F, bob, reduced };\n    kick.current();\n  }, [F, bob, reduced]);\n\n  // The float loop: labels spring toward their height (0 = landed), shadows follow.\n  useEffect(() => {\n    const root = rootRef.current;\n    if (!root) return;\n    const st = new WeakMap<Element, { y: number; v: number; i: number; b: number }>();\n    let raf = 0;\n    let last = 0;\n    let onScreen = true;\n    const t0 = performance.now();\n    const paint = (el: HTMLElement, y: number, b: number) => {\n      const label = el.querySelector<HTMLElement>(\"[data-a-label]\");\n      const shadow = el.querySelector<HTMLElement>(\"[data-a-shadow]\");\n      const glow = el.querySelector<HTMLElement>(\"[data-a-glow]\");\n      if (glow) glow.style.opacity = (Math.max(0, 1 - y / 2.4) * b).toFixed(3);\n      if (label) {\n        label.style.transform = \"translate3d(0,\" + (-y).toFixed(2) + \"px,0)\";\n        label.style.opacity = b.toFixed(3);\n      }\n      if (shadow) {\n        shadow.style.transform = \"translateX(-50%) scaleX(\" + (0.78 + 0.055 * y).toFixed(3) + \")\";\n        shadow.style.opacity = (Math.max(0.18, 0.92 - 0.075 * y) * b).toFixed(3);\n        shadow.style.filter = \"blur(\" + (0.35 + 0.42 * y).toFixed(2) + \"px)\";\n      }\n    };\n    const frame = (now: number) => {\n      raf = 0;\n      const dt = last ? Math.min(0.05, (now - last) / 1000) : 1 / 60;\n      last = now;\n      const o = opts.current;\n      const t = (now - t0) / 1000;\n      const bobbing = o.bob && !o.reduced;\n      let moving = false;\n      // The mark's dot hangs over its line and bobs on a period of its own.\n      const dot = root.querySelector<SVGElement>(\"[data-a-dot]\");\n      const dotShadow = root.querySelector<SVGElement>(\"[data-a-dotshadow]\");\n      if (dot && dotShadow) {\n        const d = bobbing ? Math.sin((t / 10.3) * Math.PI * 2) : 0;\n        dot.style.transform = \"translateY(\" + (0.7 * d).toFixed(3) + \"px)\";\n        dotShadow.style.transformOrigin = \"8px 12.4px\";\n        dotShadow.style.transform = \"scaleX(\" + (1 - 0.1 * d).toFixed(3) + \")\";\n      }\n      const floats = Array.from(root.querySelectorAll<HTMLElement>(\"[data-a-float]\"));\n      const hoverIdx = hoverEl.current ? floats.indexOf(hoverEl.current as HTMLElement) : -1;\n      floats.forEach((el, idx) => {\n        let s = st.get(el);\n        if (!s) {\n          // Born on the shelf: each label lifts off in turn and fades in.\n          s = { y: o.reduced ? o.F : 0, v: 0, i: idx, b: o.reduced ? 1 : 0 };\n          st.set(el, s);\n        }\n        const hold = !o.reduced && t < 0.15 + s.i * 0.09;\n        if (!hold && s.b < 1) s.b = Math.min(1, s.b + dt / 0.45);\n        if (hold || s.b < 1) moving = true;\n        const landed = el === hoverEl.current || (active.current !== null && el.dataset.href === active.current);\n        const period = A_PERIODS[s.i % A_PERIODS.length] ?? 11;\n        const lift = hold ? 0 : o.F + (bobbing ? 1.4 * Math.sin((t / period) * Math.PI * 2 + s.i * 1.9) : 0) + (hoverIdx >= 0 && Math.abs(idx - hoverIdx) === 1 ? 0.9 : 0);\n        const goal = landed ? 0 : lift;\n        if (o.reduced) {\n          s.y = goal;\n          s.v = 0;\n        } else {\n          // Critically damped spring, about 260 ms to land.\n          const k = 170;\n          s.v += (k * (goal - s.y) - 2 * Math.sqrt(k) * s.v) * dt;\n          s.y += s.v * dt;\n          if (Math.abs(goal - s.y) > 0.02 || Math.abs(s.v) > 0.02) moving = true;\n        }\n        paint(el, s.y, s.b);\n      });\n      if (onScreen && !document.hidden && (bobbing || moving)) raf = requestAnimationFrame(frame);\n      else last = 0;\n    };\n    kick.current = () => {\n      if (!raf) raf = requestAnimationFrame(frame);\n    };\n    const io = typeof IntersectionObserver !== \"undefined\" ? new IntersectionObserver((es) => {\n      onScreen = es.some((e) => e.isIntersecting);\n      if (onScreen) kick.current();\n    }) : null;\n    io?.observe(root);\n    const onVis = () => kick.current();\n    document.addEventListener(\"visibilitychange\", onVis);\n    const over = (e: Event) => {\n      const el = (e.target as Element | null)?.closest?.(\"[data-a-float]\") ?? null;\n      if (el !== hoverEl.current) {\n        hoverEl.current = el;\n        kick.current();\n      }\n    };\n    const out = (e: Event) => {\n      const to = (e as FocusEvent).relatedTarget as Element | null;\n      if (!to || !root.contains(to) || !to.closest(\"[data-a-float]\")) {\n        hoverEl.current = null;\n        kick.current();\n      }\n    };\n    root.addEventListener(\"pointerover\", over);\n    root.addEventListener(\"focusin\", over);\n    root.addEventListener(\"pointerout\", out);\n    root.addEventListener(\"focusout\", out);\n    kick.current();\n    return () => {\n      if (raf) cancelAnimationFrame(raf);\n      raf = 0;\n      io?.disconnect();\n      document.removeEventListener(\"visibilitychange\", onVis);\n      root.removeEventListener(\"pointerover\", over);\n      root.removeEventListener(\"focusin\", over);\n      root.removeEventListener(\"pointerout\", out);\n      root.removeEventListener(\"focusout\", out);\n      kick.current = () => {};\n    };\n  }, []);\n\n  // Scroll: the bar tightens over the first 120px; the section in view lands its link.\n  useAScrollFrame(rootRef, (f) => {\n    const root = rootRef.current;\n    const bar = barRef.current;\n    if (!root || !bar) return;\n    const sp = aScrollParent(root);\n    const y = sp ? sp.scrollTop : window.scrollY;\n    bar.style.setProperty(\"--c\", aClamp(y / 120).toFixed(3));\n    if (!track) return;\n    // The section in view: of the targets whose top has passed 40% of the view, the lowest on the page.\n    let now: string | null = null;\n    let best = -Infinity;\n    for (const l of links) {\n      const href = aSafeHref(l.href);\n      if (!href.startsWith(\"#\") || href.length < 2) continue;\n      const t = aAnchor(root, href.slice(1));\n      const top = t ? t.getBoundingClientRect().top - f.view.top : Infinity;\n      if (top <= f.view.height * 0.4 && top > best) {\n        best = top;\n        now = href;\n      }\n    }\n    if (now !== active.current) {\n      active.current = now;\n      root.querySelectorAll<HTMLElement>(\"[data-a-float]\").forEach((el) => {\n        if (now !== null && el.dataset.href === now) el.setAttribute(\"aria-current\", \"location\");\n        else el.removeAttribute(\"aria-current\");\n      });\n      kick.current();\n    }\n  });\n\n  // The phone sheet: Escape and an outside tap close it; focus goes back to the button.\n  useEffect(() => {\n    kick.current();\n    if (!open) return;\n    const close = (back: boolean) => {\n      setOpen(false);\n      if (back) menuRef.current?.focus();\n    };\n    const onKey = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") close(true);\n    };\n    const onDown = (e: PointerEvent) => {\n      const t = e.target as Node;\n      if (!sheetRef.current?.contains(t) && !menuRef.current?.contains(t)) close(false);\n    };\n    document.addEventListener(\"keydown\", onKey);\n    document.addEventListener(\"pointerdown\", onDown);\n    return () => {\n      document.removeEventListener(\"keydown\", onKey);\n      document.removeEventListener(\"pointerdown\", onDown);\n    };\n  }, [open]);\n\n  const ctaLink = cta ? (\n    <a\n      href={aSafeHref(cta.href)}\n      onClick={aGo}\n      className=\"shrink-0 rounded-[2px] bg-[#d9ecf8] px-3.5 py-2 text-[13px] leading-none font-medium whitespace-nowrap text-[#0a0e12] transition-[filter] hover:brightness-105 focus-visible:outline-2! focus-visible:outline-offset-2! focus-visible:outline-[#d9ecf8]!\"\n    >\n      {cta.label}\n    </a>\n  ) : null;\n\n  return (\n    <header ref={rootRef} className={\"@container relative w-full \" + className} style={{ fontFamily: A_SANS, ...style }}>\n      <style>{\".a-shelf{background:linear-gradient(180deg,rgba(217,236,248,.62) 0,rgba(217,236,248,.2) 1px,rgba(217,236,248,.05) 5px);-webkit-mask:linear-gradient(90deg,transparent,#000 12%,#000 88%,transparent);mask:linear-gradient(90deg,transparent,#000 12%,#000 88%,transparent)}@keyframes a-sheet{from{opacity:0;transform:translateY(-6px)}}.a-sheet{animation:a-sheet .22s cubic-bezier(.2,.7,.2,1)}@media (prefers-reduced-motion:reduce){.a-sheet{animation:none}}\"}</style>\n      <div\n        ref={barRef}\n        className=\"relative mx-auto flex w-full items-center justify-between gap-4 rounded-[2px] pl-4 backdrop-blur-[14px]\"\n        style={{\n          maxWidth: \"calc(1180px - 180px * var(--c, 0))\",\n          paddingTop: \"calc(10px - 3px * var(--c, 0))\",\n          paddingBottom: \"calc(10px - 3px * var(--c, 0))\",\n          paddingRight: \"calc(10px - 2px * var(--c, 0))\",\n          background: \"rgba(11,17,22,calc(0.5 + 0.34 * var(--c, 0)))\",\n          boxShadow: \"inset 0 1px 0 rgba(234,241,246,0.07), inset 0 0 0 1px rgba(214,232,245,0.12), 0 18px 40px -24px rgba(0,0,0,calc(0.4 + 0.4 * var(--c, 0)))\",\n        }}\n      >\n        <a\n          href={aSafeHref(homeHref)}\n          onClick={aGo}\n          className=\"flex shrink-0 items-center gap-2.5 rounded-[2px] text-[17px] leading-none font-medium tracking-[-0.035em] text-[#eaf1f6]/92 focus-visible:outline-2! focus-visible:outline-offset-4! focus-visible:outline-[#d9ecf8]!\"\n        >\n          <ALevMark />\n          {brand}\n        </a>\n        <nav aria-label=\"Main\" className=\"relative hidden items-end @3xl:flex\">\n          <span aria-hidden=\"true\" className=\"a-shelf pointer-events-none absolute inset-x-0 bottom-[5px] h-[5px]\" />\n          {links.map((l) => (\n            <AFloatLink key={l.label + l.href} link={l} />\n          ))}\n        </nav>\n        <div className=\"flex items-center gap-2\">\n          {ctaLink}\n          <button\n            ref={menuRef}\n            type=\"button\"\n            aria-expanded={open}\n            aria-controls={sheetId}\n            onClick={() => setOpen((o) => !o)}\n            className=\"rounded-[2px] px-3 py-2 text-[13px] leading-none font-medium text-[#eaf1f6] focus-visible:outline-2! focus-visible:outline-offset-2! focus-visible:outline-[#d9ecf8]! @3xl:hidden\"\n            style={{ boxShadow: \"inset 0 0 0 1px rgba(214,232,245,0.2)\" }}\n          >\n            {open ? \"Close\" : \"Menu\"}\n          </button>\n        </div>\n      </div>\n      {open && (\n        <div\n          ref={sheetRef}\n          id={sheetId}\n          className=\"a-sheet absolute inset-x-0 top-full mt-2 rounded-[2px] p-3 backdrop-blur-[14px] @3xl:hidden\"\n          style={{ background: \"rgba(11,17,22,0.9)\", boxShadow: \"inset 0 0 0 1px rgba(214,232,245,0.12), 0 24px 48px -20px rgba(0,0,0,0.7)\" }}\n        >\n          <nav aria-label=\"Main\" className=\"flex flex-col\">\n            {links.map((l) => (\n              <AFloatLink key={l.label + l.href} link={l} shelf onGo={() => setOpen(false)} />\n            ))}\n          </nav>\n        </div>\n      )}\n    </header>\n  );\n}\n","type":"registry:component","target":"components/viberdy/levitating-link-nav.tsx"}],"meta":{"tier":"free","tags":["navigation","navbar","header","levitation","shadow","scroll","anomaly","attention","dark mode"],"href":"https://viberdy.dev/library/levitating-link-nav","aiPrompt":"Add \"LevitatingLinkNav\" to this app: a dark glass nav bar whose link labels hang in the air a few pixels above a lit shelf, each over its own soft shadow. The anomaly is gravity: the words float, and the one you point at lands.\n\nHow it works:\n- A bar (radius 2px, inset hairline, backdrop blur) with the wordmark and a small mark (a dot hanging over a line), the links, and one button.\n- Under the links runs a 5px lit shelf (a bright top line fading down, masked at the ends). Each label floats `float` px above it (default 5) and bobs 1.4px on its own period (9.7, 11.3, 12.7, 13.9 s and so on), so the row never moves in step. Under each label a dark elliptical shadow sits on the shelf: the higher the label, the wider (scaleX 0.78 + 0.055h), fainter (opacity 0.92 - 0.075h) and softer (blur 0.35 + 0.42h px) it is.\n- Hover or keyboard focus lands a link: a critically damped spring (k 170, about 260 ms) drops it onto the shelf and its shadow tightens into a contact line.\n- `track` (default on): links with `#anchor` hrefs follow their sections (`[data-a-anchor=\"id\"]` in the same site, else an element with that id); of the sections whose tops have passed 40% of the view, the lowest on the page lands its link and marks it `aria-current=\"location\"`.\n- Over the first 120px of scroll (the window, or whatever box scrolls it) the bar narrows by 180px and its glass fills in, through one CSS variable.\n- Below a 768px container the links move into a sheet under the bar (a Menu / Close button with aria-expanded); Escape or an outside tap closes it, and Escape returns focus to the button.\n\nProps: `brand`, `homeHref`, `links` ({ label, href }[]), `cta` ({ label, href } or null), `track`, `float` (2-10), `bob`, `className`, `style`. Every href goes through `aSafeHref`; same-page links glide to their section.\n\nRules: one animation loop writes transforms, opacity and blur through refs (no React state per frame), and it runs only while a label moves or bobs and the bar is on screen. Reduced motion holds the labels still at their float and makes landing instant.","license":"https://viberdy.dev/license"}}