{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"bento-command-nav","type":"registry:component","title":"Bento Command Nav","description":"Site navigation as one row of bento tiles sharing 1px hairlines, with a pointer light that shows through them and menus that open as panels of tiles.","dependencies":[],"registryDependencies":[],"files":[{"path":"components/viberdy/bento-command-nav.tsx","content":"\"use client\";\n\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport type { CSSProperties, FocusEvent as ReactFocusEvent, PointerEvent as ReactPointerEvent, ReactNode } from \"react\";\n\n/**\n * BentoCommandNav — site navigation set as one compact row of bento tiles.\n *\n * The tiles share their hairlines: they sit a pixel apart on a hairline\n * ground, so every divider is the same 1px line and the row reads as one\n * object cut into cells rather than a bar with links on it. A single light\n * follows the pointer under the row and shows through those hairlines, so\n * the dividers nearest the pointer catch it. Size and luminance do the rest:\n * the current page is a raised cell, the empty cell in the middle is the\n * row's negative space, and the call to action is the only filled accent.\n *\n * An item with links opens a panel of tiles under the row, cut the same way,\n * with the first link as the big tile. It opens on hover (with intent\n * delays), on click and from the keyboard; Escape closes it and returns\n * focus. Below 56rem of container width the links fold into a Menu tile.\n *\n * Needs Tailwind v4 (container queries are built in). No dependencies beyond\n * React.\n */\n\nexport type BentoNavLink = {\n  title: string;\n  href: string;\n  body?: string;\n  /** A small mono label above the title. */\n  meta?: string;\n  /** A headline figure, shown on the big first tile only, e.g. \"1.8s\". */\n  figure?: string;\n  /** What the figure measures, in mono under it, e.g. \"median build\". */\n  figureNote?: string;\n};\nexport type BentoNavItem = {\n  label: string;\n  /** A plain link. Give `links` instead to open a panel. */\n  href?: string;\n  current?: boolean;\n  links?: BentoNavLink[];\n};\n\nexport type BentoCommandNavProps = {\n  brand: { name: string; href?: string; mark?: ReactNode };\n  items: BentoNavItem[];\n  status?: { label: string; href?: string; tone?: \"ok\" | \"busy\" | \"down\" };\n  secondary?: { label: string; href: string };\n  cta?: { label: string; href: string };\n  /** \"#rrggbb\"; the call to action is the only thing that wears it. */\n  accent?: string;\n  /** The nav landmark's accessible name. */\n  label?: string;\n  className?: string;\n};\n\nconst MONO = \"var(--font-mono, var(--font-geist-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace))\";\nconst OUT = \"cubic-bezier(0.16, 1, 0.3, 1)\";\nconst TONES = { ok: \"#2bd576\", busy: \"#ffb020\", down: \"#ff5a5a\" };\nconst MENU = \"__menu\";\n\n/** Allow relative paths, fragments, http(s), mailto and tel; anything else becomes \"#\". */\nfunction safeHref(href: string): string {\n  const v = href.replace(/[\\t\\n\\r]/g, \"\").trim();\n  if (v.indexOf(String.fromCharCode(92)) !== -1) return \"#\";\n  if (/^(https?:|mailto:|tel:)/i.test(v)) return v;\n  if (/^[/#?]/.test(v) && !/^\\/\\//.test(v)) return v;\n  return \"#\";\n}\n\n/** \"#rrggbb\" if valid, otherwise the fallback. Never interpolated unchecked. */\nfunction safeHex(hex: string | undefined, fallback: string): string {\n  return /^#[0-9a-f]{6}$/i.test(hex || \"\") ? (hex as string) : fallback;\n}\n\nfunction isLight(hex: string): boolean {\n  const n = parseInt(hex.slice(1), 16);\n  const c = [(n >> 16) & 255, (n >> 8) & 255, n & 255].map((v) => {\n    const s = v / 255;\n    return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);\n  });\n  return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2] > 0.18;\n}\n\nfunction reduced(): boolean {\n  return typeof window !== \"undefined\" && window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n}\n\n// Each state names its own background: two bg utilities on one element are\n// resolved by stylesheet order, not by the order of the class list.\nconst CELL =\n  \"flex h-full items-center whitespace-nowrap outline-none transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-white/80 motion-reduce:transition-none\";\nconst REST = \" text-[13px] font-medium bg-[#0f1012] text-white/65 hover:bg-[#16171a] hover:text-white\";\nconst RAISED = \" text-[13px] font-medium bg-[#16171a] text-white\";\nconst TILE_LINK =\n  \"group flex h-full flex-col outline-none transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-white/80 motion-reduce:transition-none\";\nconst LABEL = \"text-[11px] uppercase tracking-[0.12em] text-white/45\";\n\n/**\n * The shared-hairline ground: a 1px hairline under the cells, a light that\n * shows through it near the pointer, and a faint glow over the cells.\n */\nfunction Ground({ children, className, style }: { children: ReactNode; className: string; style?: CSSProperties }) {\n  return (\n    <>\n      <span aria-hidden className={\"pointer-events-none absolute -z-10 rounded-[20px] \" + className} style={{ ...style, background: \"rgba(255,255,255,0.08)\" }} />\n      <span\n        aria-hidden\n        data-bcn-light=\"\"\n        className={\"pointer-events-none absolute -z-10 rounded-[20px] transition-opacity duration-200 \" + className}\n        style={{\n          ...style,\n          opacity: \"var(--bcn-o, 0)\",\n          background: \"radial-gradient(180px circle at var(--bcn-x, -999px) var(--bcn-y, -999px), rgba(255,255,255,0.7), rgba(255,255,255,0) 70%)\",\n        }}\n      />\n      {children}\n      <span\n        aria-hidden\n        data-bcn-light=\"\"\n        className={\"pointer-events-none absolute rounded-[20px] transition-opacity duration-200 \" + className}\n        style={{\n          ...style,\n          opacity: \"var(--bcn-o, 0)\",\n          background: \"radial-gradient(260px circle at var(--bcn-x, -999px) var(--bcn-y, -999px), rgba(255,255,255,0.07), rgba(255,255,255,0) 70%)\",\n        }}\n      />\n    </>\n  );\n}\n\n/**\n * A panel of tiles under the row, cut with the same shared hairlines. The\n * outer box's top padding bridges the gap, so the pointer can cross it.\n */\nfunction Panel({\n  id,\n  active,\n  grid,\n  children,\n}: {\n  id: string;\n  active: boolean;\n  grid: CSSProperties;\n  children: ReactNode;\n}) {\n  return (\n    <div id={id} inert={!active} className=\"absolute inset-x-0 top-full z-30 pt-2\">\n      <div className=\"relative isolate rounded-[20px] bg-[#08090a]\">\n        <Ground className=\"inset-0\">\n          <ul role=\"list\" className=\"relative m-px grid gap-px overflow-hidden rounded-[19px]\" style={grid}>\n            {children}\n          </ul>\n        </Ground>\n      </div>\n    </div>\n  );\n}\n\nfunction Chevron({ open }: { open: boolean }) {\n  return (\n    <svg\n      aria-hidden\n      width=\"10\"\n      height=\"10\"\n      viewBox=\"0 0 10 10\"\n      className=\"transition-transform duration-300 motion-reduce:transition-none\"\n      style={{ transform: open ? \"rotate(180deg)\" : \"none\", transitionTimingFunction: OUT }}\n    >\n      <path d=\"M2 3.75 5 6.75 8 3.75\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.4\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n    </svg>\n  );\n}\n\nexport function BentoCommandNav({ brand, items, status, secondary, cta, accent, label = \"Main\", className = \"\" }: BentoCommandNavProps) {\n  const uid = useId().replace(/:/g, \"\");\n  const tint = safeHex(accent, \"#ff5a1f\");\n  const ink = isLight(tint) ? \"#0b0b0c\" : \"#ffffff\";\n\n  // `open` is what the triggers report; `shown` keeps a closing panel on\n  // screen for its exit.\n  const [open, setOpen] = useState<string | null>(null);\n  const [shown, setShown] = useState<string | null>(null);\n  const openedBy = useRef<\"hover\" | \"click\">(\"click\");\n  const timers = useRef({ open: 0, close: 0 });\n  const navRef = useRef<HTMLElement>(null);\n  const light = useRef({ x: 0, y: 0, raf: 0 });\n\n  // Parts are found by id and data attribute, only in handlers and effects.\n  const panelId = (id: string) => uid + \"-\" + id;\n  function part(sel: string): HTMLElement | null {\n    return navRef.current ? navRef.current.querySelector<HTMLElement>(sel) : null;\n  }\n\n  function clearTimers() {\n    window.clearTimeout(timers.current.open);\n    window.clearTimeout(timers.current.close);\n  }\n  function show(id: string, by: \"hover\" | \"click\") {\n    clearTimers();\n    openedBy.current = by;\n    setOpen(id);\n    setShown(id);\n  }\n  function hide(returnFocus = false) {\n    clearTimers();\n    if (returnFocus && open) part('[aria-controls=\"' + CSS.escape(panelId(open)) + '\"]')?.focus();\n    setOpen(null);\n  }\n\n  // Entrance: the panel opens downward from the row, its tiles just after.\n  useEffect(() => {\n    if (!shown || reduced()) return;\n    const el = navRef.current?.querySelector<HTMLElement>(\"#\" + CSS.escape(uid + \"-\" + shown));\n    if (!el) return;\n    const runs = [\n      el.animate([{ clipPath: \"inset(0 0 100% 0 round 20px)\" }, { clipPath: \"inset(0 0 0% 0 round 20px)\" }], { duration: 300, easing: OUT }),\n    ];\n    el.querySelectorAll(\"li\").forEach((li, i) => {\n      runs.push(\n        li.animate([{ opacity: 0, transform: \"translateY(6px)\" }, { opacity: 1, transform: \"none\" }], {\n          duration: 260,\n          delay: 40 + i * 60,\n          easing: OUT,\n          fill: \"backwards\",\n        }),\n      );\n    });\n    return () => runs.forEach((a) => a.cancel());\n  }, [shown, uid]);\n\n  // Exit: a short fade, then unmount. Reopening mid-exit cancels it.\n  useEffect(() => {\n    if (open || !shown) return;\n    const el = navRef.current?.querySelector<HTMLElement>(\"#\" + CSS.escape(uid + \"-\" + shown));\n    if (!el || reduced()) {\n      const t = window.setTimeout(() => setShown(null), 0);\n      return () => window.clearTimeout(t);\n    }\n    const a = el.animate([{ opacity: 1, transform: \"none\" }, { opacity: 0, transform: \"translateY(-4px)\" }], {\n      duration: 160,\n      easing: \"ease-out\",\n      fill: \"forwards\",\n    });\n    const t = window.setTimeout(() => setShown(null), 170);\n    return () => {\n      window.clearTimeout(t);\n      a.cancel();\n    };\n  }, [open, shown, uid]);\n\n  // A press anywhere outside the nav closes the panel.\n  useEffect(() => {\n    if (!open) return;\n    const onDown = (e: PointerEvent) => {\n      if (navRef.current && !navRef.current.contains(e.target as Node)) setOpen(null);\n    };\n    document.addEventListener(\"pointerdown\", onDown);\n    return () => document.removeEventListener(\"pointerdown\", onDown);\n  }, [open]);\n\n  useEffect(() => {\n    const t = timers.current;\n    const l = light.current;\n    return () => {\n      window.clearTimeout(t.open);\n      window.clearTimeout(t.close);\n      cancelAnimationFrame(l.raf);\n    };\n  }, []);\n\n  // One light for the row and any open panel, one write per animation frame.\n  function onMove(e: ReactPointerEvent<HTMLElement>) {\n    light.current.x = e.clientX;\n    light.current.y = e.clientY;\n    if (light.current.raf) return;\n    const root = e.currentTarget;\n    light.current.raf = requestAnimationFrame(() => {\n      light.current.raf = 0;\n      root.querySelectorAll<HTMLElement>(\"[data-bcn-light]\").forEach((el) => {\n        const r = el.getBoundingClientRect();\n        const k = el.offsetWidth / (r.width || 1);\n        el.style.setProperty(\"--bcn-x\", ((light.current.x - r.left) * k).toFixed(1) + \"px\");\n        el.style.setProperty(\"--bcn-y\", ((light.current.y - r.top) * k).toFixed(1) + \"px\");\n      });\n      root.style.setProperty(\"--bcn-o\", \"1\");\n    });\n  }\n\n  // Hover intent, for mouse pointers only: open after 80ms, close 180ms after\n  // leaving, unless the panel was opened with a click.\n  function enter(id: string, e: ReactPointerEvent) {\n    if (e.pointerType !== \"mouse\") return;\n    clearTimers();\n    if (open === id) return;\n    if (open) show(id, \"hover\");\n    else timers.current.open = window.setTimeout(() => show(id, \"hover\"), 80);\n  }\n  function leave(e: ReactPointerEvent) {\n    if (e.pointerType !== \"mouse\") return;\n    window.clearTimeout(timers.current.open);\n    if (open && openedBy.current === \"hover\") timers.current.close = window.setTimeout(() => setOpen(null), 180);\n  }\n  function press(id: string) {\n    if (open === id && openedBy.current === \"click\") hide();\n    else show(id, \"click\");\n  }\n  // Focus moving to another cell closes the open panel.\n  function onFocusIn(e: ReactFocusEvent) {\n    if (!open) return;\n    const cell = part('[data-bcn-cell=\"' + CSS.escape(open) + '\"]');\n    if (cell && !cell.contains(e.target as Node)) setOpen(null);\n  }\n\n\n  function linkPanel(item: BentoNavItem, id: string) {\n    const links = item.links ?? [];\n    const rest = links.slice(1);\n    const cols = Math.max(1, Math.ceil(rest.length / 2));\n    return (\n      <Panel\n        id={panelId(id)}\n        active={open === id}\n        grid={{\n          gridTemplateColumns: rest.length ? \"minmax(0,1.3fr) repeat(\" + cols + \", minmax(0,1fr))\" : \"minmax(0,1fr)\",\n          gridAutoRows: \"minmax(104px, auto)\",\n        }}\n      >\n        {links.map((l, i) => {\n          const big = i === 0;\n          // Fill the block: an odd one out spans the empty cell it would leave.\n          const odd = !big && rest.length % 2 === 1 && i === links.length - 1;\n          const span = big ? \"row-span-2\" : odd ? (rest.length === 1 ? \"row-span-2\" : \"col-span-2\") : \"\";\n          return (\n            <li key={l.title + i} className={span}>\n              <a href={safeHref(l.href)} className={TILE_LINK + \" bg-[#0f1012] hover:bg-[#16171a]\" + (big ? \" p-6\" : \" p-5\")}>\n                {l.meta && (\n                  <span className={LABEL} style={{ fontFamily: MONO }}>\n                    {l.meta}\n                  </span>\n                )}\n                <span className={(big ? \"mt-2 text-[20px]\" : \"mt-1.5 text-[15px]\") + \" font-semibold tracking-[-0.01em] text-white\"}>\n                  {l.title}\n                </span>\n                {l.body && <span className={(big ? \"mt-2 text-[14px]\" : \"mt-1 text-[13px]\") + \" leading-snug text-white/55\"}>{l.body}</span>}\n                <span className=\"mt-auto flex items-end justify-between gap-3 pt-4\">\n                  {big && l.figure ? (\n                    <span className=\"flex flex-col gap-1.5\">\n                      <span className=\"text-[44px] font-bold leading-none tracking-[-0.04em] text-white\" style={{ fontVariantNumeric: \"tabular-nums\" }}>\n                        {l.figure}\n                      </span>\n                      {l.figureNote && (\n                        <span className={LABEL} style={{ fontFamily: MONO }}>\n                          {l.figureNote}\n                        </span>\n                      )}\n                    </span>\n                  ) : (\n                    <span />\n                  )}\n                  <span\n                    aria-hidden\n                    className=\"text-white/40 transition-[transform,color] duration-200 group-hover:translate-x-0.5 group-hover:text-white motion-reduce:transition-none\"\n                  >\n                    →\n                  </span>\n                </span>\n              </a>\n            </li>\n          );\n        })}\n      </Panel>\n    );\n  }\n\n  // The folded menu: every destination as a tile, two to a row.\n  const flat: (BentoNavLink & { current?: boolean })[] = [];\n  items.forEach((it) => {\n    if (it.links && it.links.length) it.links.forEach((l) => flat.push({ ...l, meta: it.label, body: undefined, figure: undefined, figureNote: undefined }));\n    else if (it.href) flat.push({ title: it.label, href: it.href, current: it.current });\n  });\n  if (secondary) flat.push({ title: secondary.label, href: secondary.href });\n\n  const tone = TONES[status?.tone ?? \"ok\"];\n  const statusCls = \"hidden items-center gap-2 whitespace-nowrap text-[11px] uppercase tracking-[0.12em] text-white/50 @5xl:flex\";\n  const statusBody = status && (\n    <>\n      <span aria-hidden className=\"size-1.5 shrink-0 rounded-full\" style={{ background: tone }} />\n      {status.label}\n    </>\n  );\n\n  return (\n    <nav\n      ref={navRef}\n      aria-label={label}\n      className={\"@container relative isolate w-full text-white \" + className}\n      style={{ \"--bcn-o\": \"0\" } as CSSProperties}\n      onPointerMove={onMove}\n      onPointerLeave={(e) => {\n        cancelAnimationFrame(light.current.raf);\n        light.current.raf = 0;\n        e.currentTarget.style.setProperty(\"--bcn-o\", \"0\");\n      }}\n      onKeyDown={(e) => {\n        if (e.key === \"Escape\" && open) {\n          e.preventDefault();\n          hide(true);\n        }\n      }}\n      onFocus={onFocusIn}\n    >\n      <Ground className=\"inset-x-0 top-0 h-14\">\n        <ul role=\"list\" className=\"m-px flex h-[54px] gap-px overflow-hidden rounded-[19px]\">\n          <li data-bcn-cell=\"brand\" className=\"flex\">\n            <a href={safeHref(brand.href ?? \"/\")} className={CELL + \" gap-2.5 bg-[#0f1012] pl-4 pr-5 text-[14px] font-semibold text-white hover:bg-[#16171a]\"}>\n              {brand.mark ?? (\n                <span aria-hidden className=\"grid size-6 place-items-center rounded-[7px] bg-white text-[12px] font-bold text-black\">\n                  {brand.name.slice(0, 1)}\n                </span>\n              )}\n              {brand.name}\n            </a>\n          </li>\n\n          {items.map((it, i) => {\n            const id = \"i\" + i;\n            if (it.links && it.links.length) {\n              const isOpen = open === id;\n              return (\n                <li\n                  key={it.label + i}\n                  data-bcn-cell={id}\n                  className=\"hidden @4xl:flex\"\n                  onPointerEnter={(e) => enter(id, e)}\n                  onPointerLeave={leave}\n                >\n                  <button\n                    type=\"button\"\n                    aria-expanded={isOpen}\n                    aria-controls={panelId(id)}\n                    onClick={() => press(id)}\n                    className={CELL + \" gap-2 px-4\" + (isOpen ? RAISED : REST)}\n                  >\n                    {it.label}\n                    <Chevron open={isOpen} />\n                  </button>\n                  {shown === id && linkPanel(it, id)}\n                </li>\n              );\n            }\n            return (\n              <li key={it.label + i} data-bcn-cell={id} className=\"hidden @4xl:flex\">\n                <a\n                  href={safeHref(it.href ?? \"#\")}\n                  aria-current={it.current ? \"page\" : undefined}\n                  className={CELL + \" gap-2 px-4\" + (it.current ? RAISED : REST)}\n                >\n                  {it.current && <span aria-hidden className=\"size-1 rounded-full bg-white/80\" />}\n                  {it.label}\n                </a>\n              </li>\n            );\n          })}\n\n          {/* The row's negative space, and the status when there is room. */}\n          <li data-bcn-cell=\"fill\" className=\"flex min-w-6 flex-1\">\n            <div className=\"flex h-full flex-1 items-center justify-end bg-[#0f1012] px-4\">\n              {status && status.href ? (\n                <a\n                  href={safeHref(status.href)}\n                  className={statusCls + \" rounded-[6px] outline-none transition-colors duration-200 hover:text-white focus-visible:ring-2 focus-visible:ring-white/80\"}\n                  style={{ fontFamily: MONO }}\n                >\n                  {statusBody}\n                </a>\n              ) : status ? (\n                <span className={statusCls} style={{ fontFamily: MONO }}>\n                  {statusBody}\n                </span>\n              ) : null}\n            </div>\n          </li>\n\n          {secondary && (\n            <li data-bcn-cell=\"secondary\" className=\"hidden @4xl:flex\">\n              <a href={safeHref(secondary.href)} className={CELL + \" px-4\" + REST}>\n                {secondary.label}\n              </a>\n            </li>\n          )}\n\n          {flat.length > 0 && (\n            <li data-bcn-cell={MENU} className=\"flex @4xl:hidden\">\n              <button\n                type=\"button\"\n                aria-expanded={open === MENU}\n                aria-controls={panelId(MENU)}\n                onClick={() => press(MENU)}\n                className={CELL + \" gap-2 px-4\" + (open === MENU ? RAISED : REST)}\n              >\n                <svg aria-hidden width=\"14\" height=\"14\" viewBox=\"0 0 14 14\">\n                  <path\n                    d={open === MENU ? \"M3 3l8 8M11 3l-8 8\" : \"M2 5h10M2 9h10\"}\n                    fill=\"none\"\n                    stroke=\"currentColor\"\n                    strokeWidth=\"1.5\"\n                    strokeLinecap=\"round\"\n                  />\n                </svg>\n                Menu\n              </button>\n              {shown === MENU && (\n                <Panel id={panelId(MENU)} active={open === MENU} grid={{ gridTemplateColumns: \"repeat(2, minmax(0,1fr))\", gridAutoRows: \"minmax(68px, auto)\" }}>\n                  {flat.map((l, i) => (\n                    <li key={l.title + i} className={flat.length % 2 === 1 && i === flat.length - 1 ? \"col-span-2\" : \"\"}>\n                      <a\n                        href={safeHref(l.href)}\n                        aria-current={l.current ? \"page\" : undefined}\n                        className={TILE_LINK + \" justify-center p-4\" + (l.current ? \" bg-[#16171a]\" : \" bg-[#0f1012] hover:bg-[#16171a]\")}\n                      >\n                        {l.meta && (\n                          <span className={LABEL} style={{ fontFamily: MONO }}>\n                            {l.meta}\n                          </span>\n                        )}\n                        <span className=\"mt-1 flex items-center gap-2 text-[15px] font-semibold text-white\">\n                          {l.current && <span aria-hidden className=\"size-1 rounded-full bg-white/80\" />}\n                          {l.title}\n                        </span>\n                      </a>\n                    </li>\n                  ))}\n                </Panel>\n              )}\n            </li>\n          )}\n\n          {cta && (\n            <li data-bcn-cell=\"cta\" className=\"flex\">\n              <a\n                href={safeHref(cta.href)}\n                className=\"group flex h-full items-center gap-2 whitespace-nowrap px-5 text-[13px] font-semibold outline-none transition-[filter] duration-200 hover:brightness-110 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-white/80 motion-reduce:transition-none\"\n                style={{ background: tint, color: ink }}\n              >\n                {cta.label}\n                <span aria-hidden className=\"transition-transform duration-200 group-hover:translate-x-0.5 motion-reduce:transition-none\">\n                  →\n                </span>\n              </a>\n            </li>\n          )}\n        </ul>\n      </Ground>\n    </nav>\n  );\n}\n","type":"registry:component","target":"components/viberdy/bento-command-nav.tsx"}],"meta":{"tier":"free","tags":["navigation","navbar","mega menu","bento","header","disclosure"],"href":"https://viberdy.dev/library/bento-command-nav","aiPrompt":"Create a React + Tailwind v4 client component, BentoCommandNav: site navigation set as one compact row of dark bento tiles that share their hairlines.\n\nTypes: BentoNavLink = { title, href, body?, meta? (small mono label), figure? (a headline figure, first tile only), figureNote? (what it measures, in mono under it) }; BentoNavItem = { label, href? (plain link), current?, links? (opens a panel instead) }. Props: brand { name, href?, mark? (ReactNode; default a 24px white rounded square with the first letter) }, items, status? { label, href?, tone: \"ok\" | \"busy\" | \"down\" }, secondary? { label, href }, cta? { label, href }, accent (\"#rrggbb\", regex-validated, default #ff5a1f), label (the nav's aria-label, default \"Main\"), className. Hrefs are sanitised: strip tabs and newlines, reject backslashes, allow http(s), mailto, tel and relative paths starting / # ? but not //.\n\nKit tokens: canvas #08090a; tiles #0f1012; raised #16171a; hairline white 0.08; outer radius 20 with the inner clip at 19 (concentric at a 1px inset); mono labels 11px uppercase +0.12em in var(--font-mono, var(--font-geist-mono, ui-monospace, …)); figures bold grotesque with tabular numbers; no shadows. The call to action is the only filled accent (text dark or white by the accent's luminance); the status dot is the only other colour.\n\nShared hairlines: the <nav> is a Tailwind `@container`, position relative, isolation isolate. Behind the row (absolute, z-index -1, radius 20, 56px tall): a hairline layer at white 0.08, and a light layer, radial-gradient(180px circle at var(--bcn-x) var(--bcn-y), white 0.7 → transparent 70%), at opacity var(--bcn-o) with a 200ms transition. The row is a <ul role=\"list\"> with 1px margin, 54px tall, display flex, gap 1px, overflow hidden, radius 19; the cells are opaque tiles, so the light shows only through the 1px gaps and the outer ring. Over the row, a glow layer (radial 260px, white 0.07, same opacity). On pointermove over the nav, coalesced to one write per animation frame, set --bcn-x/--bcn-y on every light layer from its own bounding rect (times offsetWidth / rect width, so it works inside scaled containers) and --bcn-o to 1 on the nav; on leave, cancel any pending frame and set it to 0.\n\nCells, in order: brand (14px, 600); each item (13px, 500, white 65%, hover raised and white; the current one raised, white, with a 4px white dot and aria-current=\"page\"); an empty flex-1 tile, the row's negative space, holding the status right-aligned from @5xl (mono 11px with a 6px dot: ok #2bd576, busy #ffb020, down #ff5a5a); secondary; a Menu tile below @4xl; the CTA filled with the accent, with an arrow that slides 2px on hover. Items and secondary show from @4xl only. Give each state its own background class rather than stacking two bg utilities. Focus: an inset 2px ring at white 80%.\n\nPanels: an item with links renders a disclosure button (aria-expanded, aria-controls, a chevron that turns 180°) whose panel sits in the DOM inside the item (so Tab reaches it next) but is absolutely positioned against the nav: inset-x 0, top 100%, z-index 30, padding-top 8px (a hover bridge). The panel is cut the same way: an isolated #08090a box with its own hairline, light and glow layers, and a grid with 1px gaps, radius 19, rows minmax(104px, auto). The first link is the big tile (row-span-2, column 1.3fr, 20px title, 14px body, the figure at 44px bold with its note under it in mono); the rest fill ceil(n/2) columns in two rows; when the rest are odd, the last spans the cell it would leave empty (two columns, or two rows if it is alone). Each tile is a link: mono meta, title, body at 55% white, an arrow bottom-right that slides on hover. Below @4xl, the Menu tile opens the same kind of panel with every destination as a tile, two to a row (panel links carry their item's label as meta), plus the secondary link.\n\nBehaviour: hover intent for mouse pointers only (open after 80ms, switch at once if another panel is open, close 180ms after leaving unless it was opened by click); click toggles; Escape closes and returns focus to the trigger; a pointerdown outside the nav or focus moving to another cell closes it. Open: clip-path inset(0 0 100% 0 round 20px) to inset(0 round 20px) over 300ms cubic-bezier(0.16,1,0.3,1), each tile rising 6px and fading in 60ms apart. Close: fade and rise 4px over 160ms, then unmount on a 170ms timer; the closing panel is inert, and reopening cancels the exit. Skip all motion under prefers-reduced-motion.\n\nOutput one self-contained \"use client\" TSX file with no dependencies beyond React; export the BentoNavLink and BentoNavItem types. Build strings with + rather than template literals.","license":"https://viberdy.dev/license"}}