{"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-AQ6MIFNX.cjs","../src/hooks/ui/use-auto-limit-tags.ts","../src/hooks/ui/use-debounce.ts","../src/hooks/ui/use-deferred-error.ts","../src/hooks/ui/use-focus-trap.ts","../src/hooks/ui/use-header-height.ts","../src/hooks/ui/use-horizontal-scrollbar.ts","../src/hooks/ui/use-image-edge-color.ts","../src/hooks/ui/use-is-truncated.ts","../src/hooks/ui/use-isomorphic-layout-effect.ts","../src/hooks/ui/use-keyboard-collision-padding.ts","../src/hooks/ui/use-local-storage.ts","../src/hooks/ui/use-marquee-engine.ts","../src/hooks/ui/use-media-query.ts","../src/hooks/ui/use-memoized-callback.ts","../src/hooks/ui/use-notification-permission.ts","../src/hooks/ui/use-onboarding-state.ts","../src/utils/onboarding-storage.ts","../src/hooks/ui/use-reset-on-page-hidden.ts","../src/hooks/ui/use-search.ts","../src/hooks/ui/use-suppress-clone-focus.ts","../src/hooks/ui/use-table-pagination.ts","../src/hooks/ui/use-throttle.ts","../src/hooks/ui/use-window-size.ts","../src/hooks/ui/index.ts","../src/hooks/platform/use-platform-config.ts","../src/utils/platform-config.tsx","../src/hooks/use-toast.ts","../src/components/ui/toaster.tsx","../src/types/tool.types.ts","../src/components/tool-icon.tsx","../src/hooks/use-contact-submission.ts","../src/utils/local-storage-adapter.ts","../src/utils/app-config.ts","../src/utils/embed-proxy-auth-storage.ts","../src/utils/embed-authed-fetch.ts","../src/utils/embed-content-fetch.ts","../src/hooks/use-copy-to-clipboard.ts","../src/hooks/use-batch-images.ts","../src/hooks/use-authenticated-image.ts","../src/hooks/use-authed-asset-src.ts","../src/hooks/use-authed-image-src.ts","../src/hooks/state/use-query-params.ts","../src/hooks/state/graphql-parser.ts","../src/hooks/state/flatten-schema.ts","../src/hooks/state/url-converter.ts","../src/hooks/state/introspection.ts","../src/hooks/state/use-api-params.ts","../src/hooks/state/use-cursor-pagination-state.ts","../src/hooks/nats/use-nats-client.ts","../src/hooks/use-near-viewport.ts","../src/hooks/use-access-code-integration.ts","../src/utils/access-code-client.ts","../src/hooks/use-og-placeholder-url.ts","../src/utils/og-placeholder.ts","../src/hooks/use-claude-mirror-src.ts","../src/utils/embed-url-converters.ts","../src/hooks/use-scroll-to-hash.ts","../src/utils/scroll-into-view.ts","../src/utils/markdown-fences.ts","../src/utils/markdown-heading-id.ts","../src/utils/anchor-id.ts","../src/utils/same-page-hash-nav.ts","../src/hooks/use-humanity-signals.ts","../src/utils/humanity-signals.ts"],"names":["measure","useState","useEffect","useRef","header","bar","useCallback","r","g","b","dismissOnboarding","markMultipleComplete","platforms","sonnerToast","jsx","Fragment","adapter","toast","useMemo","fetchPromise","introspector","isInputObjectType","clearParams"],"mappings":"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R;AACuB,EAAA;AAGnBH,EAAAA;AAECG,EAAAA;AACO,IAAA;AACJ,MAAA;AACD,MAAA;AAEnB,IAAA;AACC,IAAA;AACH,EAAA;AAEqBA,EAAAA;AACF,IAAA;AACN,IAAA;AACR,EAAA;AAEI,EAAA;AACX;A/D0hIwB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-AQ6MIFNX.cjs","sourcesContent":[null,"\"use client\"\n\nimport { useCallback, useEffect, useRef, useState } from \"react\"\n\nexport interface UseAutoLimitTagsOptions {\n  /** Total number of tags */\n  count: number\n  /** Fixed limit or \"auto\" for DOM-based measurement. Default \"auto\" */\n  limitTags?: number | \"auto\"\n  /** Placeholder text used to reserve input width (only used in \"auto\" mode) */\n  placeholder?: string\n  /**\n   * Reserve trailing space for a text input (the autocomplete combobox). Set\n   * `false` for a pure chip row that has no input — then only the chips and the\n   * \"+N\" / \"⋯\" overflow badge compete for the available width. Default `true`.\n   */\n  reserveInputWidth?: boolean\n}\n\nexport interface UseAutoLimitTagsReturn {\n  /** How many tags to show */\n  visibleCount: number\n  /** Ref for the zone that contains tags + input (must have overflow-hidden, gap, padding) */\n  middleRef: React.RefObject<HTMLDivElement | null>\n  /** Ref for the off-screen container that holds measurement copies of ALL tags */\n  measureRef: React.RefObject<HTMLDivElement | null>\n  /** Ref for the hidden span that measures placeholder text width */\n  textMeasureRef: React.RefObject<HTMLSpanElement | null>\n  /** Ref for the \"+N\" badge element (used to measure its width) */\n  badgeRef: React.RefObject<HTMLButtonElement | null>\n  /** Ref for the input element (used to read its min-width) */\n  inputRef: React.RefObject<HTMLInputElement | null>\n}\n\n/**\n * Calculates how many tags fit in a single-line container.\n *\n * Requires three off-screen measurement elements rendered by the consumer:\n * 1. A `<div ref={measureRef}>` containing Tag copies for every item (to measure widths)\n * 2. A `<span ref={textMeasureRef}>` containing the placeholder text (to reserve input width)\n * 3. The `<button ref={badgeRef}>` for the \"+N\" badge (to measure badge width)\n *\n * The hook reads real CSS values (padding, gap) from the middleRef container,\n * so it works regardless of responsive breakpoints or custom styling.\n */\nexport function useAutoLimitTags({\n  count,\n  limitTags = \"auto\",\n  placeholder = \"\",\n  reserveInputWidth = true,\n}: UseAutoLimitTagsOptions): UseAutoLimitTagsReturn {\n  const middleRef = useRef<HTMLDivElement>(null)\n  const measureRef = useRef<HTMLDivElement>(null)\n  const textMeasureRef = useRef<HTMLSpanElement>(null)\n  const badgeRef = useRef<HTMLButtonElement>(null)\n  const inputRef = useRef<HTMLInputElement>(null)\n  const [visibleCount, setVisibleCount] = useState(count)\n\n  const recalculate = useCallback(() => {\n    // Fixed limit — skip DOM measurement\n    if (limitTags !== \"auto\") {\n      setVisibleCount(Math.min(limitTags, count))\n      return\n    }\n\n    const middle = middleRef.current\n    const measure = measureRef.current\n    if (!middle || !measure) {\n      setVisibleCount(count)\n      return\n    }\n    if (count === 0) {\n      setVisibleCount(0)\n      return\n    }\n\n    // Read real CSS metrics from the middle zone\n    const cs = getComputedStyle(middle)\n    const padL = parseFloat(cs.paddingLeft) || 0\n    const padR = parseFloat(cs.paddingRight) || 0\n    const gap = parseFloat(cs.gap) || 0\n    const middleW = middle.clientWidth\n\n    // Reserve trailing space for the input (autocomplete only). A pure chip row\n    // (`reserveInputWidth: false`) has no input, so nothing — and no gap before\n    // it — is reserved.\n    let inputReservedW = 0\n    let trailingGap = 0\n    if (reserveInputWidth) {\n      const textW = textMeasureRef.current?.offsetWidth ?? 60\n      const inputMinW = inputRef.current\n        ? parseFloat(getComputedStyle(inputRef.current).minWidth) || 60\n        : 60\n      inputReservedW = Math.max(textW + 8, inputMinW)\n      trailingGap = gap\n    }\n\n    // Available = middle zone − padding − input reserved − gap before input\n    const available = middleW - padL - padR - inputReservedW - trailingGap\n\n    // Measure every tag from the off-screen container\n    const tagEls = Array.from(measure.children) as HTMLElement[]\n    const widths = tagEls.map((el) => el.offsetWidth)\n\n    // Fast check: do ALL tags fit?\n    let total = 0\n    for (let i = 0; i < widths.length; i++) {\n      total += widths[i] + (i > 0 ? gap : 0)\n    }\n    if (total <= available) {\n      setVisibleCount(count)\n      return\n    }\n\n    // Not all fit → reserve space for the \"+N\" badge\n    const badgeW = badgeRef.current?.offsetWidth ?? 40\n    const spaceWithBadge = available - badgeW - gap\n\n    let used = 0\n    let fitCount = 0\n    for (let i = 0; i < widths.length; i++) {\n      const need = widths[i] + (i > 0 ? gap : 0)\n      if (used + need > spaceWithBadge) break\n      used += need\n      fitCount++\n    }\n\n    setVisibleCount(Math.max(0, fitCount))\n  }, [count, limitTags, placeholder, reserveInputWidth])\n\n  // Recalculate when inputs change\n  useEffect(() => {\n    recalculate()\n  }, [recalculate])\n\n  // Recalculate on container resize\n  useEffect(() => {\n    const el = middleRef.current\n    if (!el) return\n    const ro = new ResizeObserver(recalculate)\n    ro.observe(el)\n    return () => ro.disconnect()\n  }, [recalculate])\n\n  return { visibleCount, middleRef, measureRef, textMeasureRef, badgeRef, inputRef }\n}\n","\"use client\"\n\nimport { useState, useEffect } from \"react\"\n\n/**\n * Hook to debounce a value\n * @param value - Value to debounce\n * @param delay - Delay in milliseconds\n * @returns Debounced value\n */\nexport function useDebounce<T>(value: T, delay = 500): T {\n  const [debouncedValue, setDebouncedValue] = useState<T>(value)\n\n  useEffect(() => {\n    // Set up timeout to update debounced value\n    const timer = setTimeout(() => {\n      setDebouncedValue(value)\n    }, delay)\n\n    // Clean up timeout on value change or unmount\n    return () => {\n      clearTimeout(timer)\n    }\n  }, [value, delay])\n\n  return debouncedValue\n}\n","'use client'\n\nimport * as React from 'react'\n\nexport interface UseDeferredErrorResult {\n  /** The message to render (undefined while the reveal is deferred or the field is valid). */\n  error: string | undefined\n  /** Attach to the field's onBlur — reveals validation immediately and keeps it live from then on. */\n  onBlur: () => void\n}\n\n/**\n * Defers showing a field validation message while the user is still typing.\n *\n * The message is revealed when the field loses focus (touched) or when the\n * value hasn't changed for `delay` ms; every keystroke restarts the timer.\n * Once touched, validation stays live. A message that clears (the value became\n * valid) disappears immediately.\n */\nexport function useDeferredError(error: string | undefined, value: string, delay = 1500): UseDeferredErrorResult {\n  const [touched, setTouched] = React.useState(false)\n  const [paused, setPaused] = React.useState(false)\n\n  // Typing hides the message and restarts the pause timer.\n  React.useEffect(() => {\n    if (touched) return\n    setPaused(false)\n    const timer = setTimeout(() => setPaused(true), delay)\n    return () => clearTimeout(timer)\n  }, [value, delay, touched])\n\n  const onBlur = React.useCallback(() => setTouched(true), [])\n\n  return {\n    error: error && (touched || paused) ? error : undefined,\n    onBlur,\n  }\n}\n","'use client'\n\nimport { useEffect, useRef, type RefObject } from 'react'\n\nconst FOCUSABLE_SELECTOR = [\n  'a[href]',\n  'button:not([disabled])',\n  'input:not([disabled])',\n  'select:not([disabled])',\n  'textarea:not([disabled])',\n  '[tabindex]:not([tabindex=\"-1\"])',\n].join(', ')\n\nexport interface UseFocusTrapOptions {\n  /** Called when Escape is pressed while focus is inside the container.\n   *  When omitted, Escape passes through untouched so document-level\n   *  listeners still fire. */\n  onEscape?: () => void\n  /**\n   * When false, Tab/Shift+Tab are NOT cycled within the container — for\n   * non-modal dialogs (e.g. SlidingSidebar, whose z-50 header stays clickable\n   * above the open drawer). Escape / initial focus / guarded restore still apply.\n   * @default true\n   */\n  contain?: boolean\n}\n\n/**\n * Focus management for overlay surfaces (mobile nav panel, sliding sidebar,\n * burger menu, modals): initial focus on activate, container-scoped Tab\n * cycling + Escape, and guarded focus restore on deactivate.\n *\n * CONTRACT: the container element MUST have `tabIndex={-1}`. After a\n * click/tap on a non-focusable area inside the surface, browsers move focus\n * to the nearest focusable ancestor — with the container script-focusable\n * that ancestor is the container itself, so this keydown listener keeps\n * hearing Escape/Tab. Without it, focus falls to `body` and the trap goes\n * deaf.\n *\n * The listener being container-scoped makes it stacking-safe: it only fires\n * while focus is inside the surface, so a modal open above never leaks its\n * Escape into the surface below.\n */\nexport function useFocusTrap(\n  containerRef: RefObject<HTMLElement | null>,\n  active: boolean,\n  { onEscape, contain = true }: UseFocusTrapOptions = {}\n): void {\n  // Latest-ref so unstable inline callbacks never tear the trap down and\n  // re-yank focus — same stable-callback discipline as `useCloseOnNavigation`.\n  const onEscapeRef = useRef(onEscape)\n  onEscapeRef.current = onEscape\n\n  useEffect(() => {\n    if (!active) return\n    const container = containerRef.current\n    if (!container) return\n\n    const previouslyFocused = document.activeElement as HTMLElement | null\n\n    const focusables = () =>\n      Array.from(container.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR))\n        .filter((el) => el.getClientRects().length > 0)\n\n    const first = focusables()[0]\n    ;(first ?? container).focus({ preventScroll: true })\n\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (e.key === 'Escape') {\n        // No preventDefault/stopPropagation — pass-through by design.\n        onEscapeRef.current?.()\n        return\n      }\n      if (e.key !== 'Tab' || !contain) return\n      const items = focusables()\n      if (items.length === 0) {\n        e.preventDefault()\n        container.focus({ preventScroll: true })\n        return\n      }\n      const firstEl = items[0]\n      const lastEl = items[items.length - 1]\n      const current = document.activeElement\n      if (e.shiftKey && (current === firstEl || current === container)) {\n        e.preventDefault()\n        lastEl.focus({ preventScroll: true })\n      } else if (!e.shiftKey && current === lastEl) {\n        e.preventDefault()\n        firstEl.focus({ preventScroll: true })\n      }\n    }\n\n    container.addEventListener('keydown', handleKeyDown)\n\n    return () => {\n      container.removeEventListener('keydown', handleKeyDown)\n      // Guarded restore: only when focus is still inside the surface (or fell\n      // to body because the surface unmounted) — never steal focus from a\n      // dialog that opened meanwhile (e.g. the nav-footer → SSO modal flow).\n      const current = document.activeElement\n      if (\n        previouslyFocused &&\n        previouslyFocused.isConnected &&\n        (current === null || current === document.body || container.contains(current))\n      ) {\n        previouslyFocused.focus({ preventScroll: true })\n      }\n    }\n  }, [containerRef, active, contain])\n}\n","'use client'\n\nimport { useEffect, useState } from 'react'\n\n/**\n * Returns the combined height (px) of the sticky page header and announcement\n * bar (if present), updated live via ResizeObserver / MutationObserver.\n * Useful for offsetting fixed/absolute-positioned panels so they don't\n * overlap the header.\n */\nexport function useHeaderHeight(\n  // First-paint fallback = the unified top-navigation `big` bar (72px), the\n  // size every hub/marketing site renders; replaced by the live measurement.\n  defaultHeight = 72,\n  options: {\n    /**\n     * When false, no observers are attached and the default height is\n     * returned — for consumers that stay mounted on every page but only need\n     * the measurement while an overlay is open (avoids an always-on\n     * document-wide MutationObserver + forced reflow).\n     * @default true\n     */\n    enabled?: boolean\n  } = {}\n): number {\n  const { enabled = true } = options\n  const [height, setHeight] = useState(defaultHeight)\n\n  useEffect(() => {\n    if (!enabled) return\n\n    const measure = () => {\n      let total = 0\n      const header = document.querySelector('header')\n      if (header) total += header.offsetHeight\n      const bar = document.querySelector('[data-announcement-bar]')\n      if (bar instanceof HTMLElement) total += bar.offsetHeight\n      setHeight(total > 0 ? total : defaultHeight)\n    }\n\n    measure()\n\n    const resizeObserver = new ResizeObserver(measure)\n    const header = document.querySelector('header')\n    if (header) resizeObserver.observe(header)\n    const bar = document.querySelector('[data-announcement-bar]')\n    if (bar) resizeObserver.observe(bar)\n\n    const mutationObserver = new MutationObserver((mutations) => {\n      for (const mutation of mutations) {\n        if (mutation.type === 'childList' || mutation.type === 'attributes') {\n          measure()\n          const newBar = document.querySelector('[data-announcement-bar]')\n          if (newBar) resizeObserver.observe(newBar)\n        }\n      }\n    })\n    mutationObserver.observe(document.body, {\n      childList: true,\n      subtree: true,\n      attributes: true,\n      attributeFilter: ['data-announcement-bar'],\n    })\n\n    return () => {\n      resizeObserver.disconnect()\n      mutationObserver.disconnect()\n    }\n  }, [defaultHeight, enabled])\n\n  return height\n}\n","'use client'\n\nimport { useRef, useState, useCallback } from 'react'\n\n/**\n * Custom horizontal scrollbar hook that drives a plain-DOM scrollbar.\n *\n * Returns a callback ref (`scrollRef`) to attach to the scrollable container,\n * plus refs, state, and event handlers for the track + thumb.\n *\n * Uses a **callback ref** so measurement and ResizeObserver setup happen\n * automatically when the scroll element mounts — even if it renders later\n * (e.g. after async data loads).\n */\nexport function useHorizontalScrollbar() {\n  const scrollElRef = useRef<HTMLDivElement | null>(null)\n  const trackRef = useRef<HTMLDivElement>(null)\n  const thumbRef = useRef<HTMLDivElement>(null)\n  const roRef = useRef<ResizeObserver | null>(null)\n  const moRef = useRef<MutationObserver | null>(null)\n  const [thumbRatio, setThumbRatio] = useState(0)\n\n  // Edge-fade state\n  const [canScrollLeft, setCanScrollLeft] = useState(false)\n  const [canScrollRight, setCanScrollRight] = useState(false)\n  const prevCanScrollLeftRef = useRef(false)\n  const prevCanScrollRightRef = useRef(false)\n\n  const isDraggingRef = useRef(false)\n  const dragStartRef = useRef({ mouseX: 0, scrollLeft: 0 })\n  const rafIdRef = useRef<number>(0)\n\n  /** Compute the thumb left% from current scrollLeft and apply to DOM directly */\n  const syncThumbToDOM = useCallback(() => {\n    const el = scrollElRef.current\n    const thumb = thumbRef.current\n    if (!el || !thumb) return\n\n    const maxScroll = el.scrollWidth - el.clientWidth\n    if (maxScroll <= 0) {\n      thumb.style.left = '0%'\n      return\n    }\n\n    const ratio = el.clientWidth / el.scrollWidth\n    const fraction = Math.min(Math.max(el.scrollLeft, 0), maxScroll) / maxScroll\n    thumb.style.left = `${fraction * (1 - ratio) * 100}%`\n  }, [])\n\n  const syncEdgeFades = useCallback(() => {\n    const el = scrollElRef.current\n    if (!el) return\n\n    const maxScroll = el.scrollWidth - el.clientWidth\n    const ratio = el.clientWidth / el.scrollWidth\n    if (ratio >= 1 || maxScroll <= 0) {\n      if (prevCanScrollLeftRef.current) {\n        prevCanScrollLeftRef.current = false\n        setCanScrollLeft(false)\n      }\n      if (prevCanScrollRightRef.current) {\n        prevCanScrollRightRef.current = false\n        setCanScrollRight(false)\n      }\n      return\n    }\n\n    const fraction = Math.min(Math.max(el.scrollLeft, 0), maxScroll) / maxScroll\n    const left = fraction > 0.001\n    const right = fraction < 0.999\n\n    if (left !== prevCanScrollLeftRef.current) {\n      prevCanScrollLeftRef.current = left\n      setCanScrollLeft(left)\n    }\n    if (right !== prevCanScrollRightRef.current) {\n      prevCanScrollRightRef.current = right\n      setCanScrollRight(right)\n    }\n  }, [])\n\n  const measure = useCallback(() => {\n    const el = scrollElRef.current\n    if (!el) return\n    const ratio = el.clientWidth / el.scrollWidth\n    setThumbRatio(ratio >= 1 ? 0 : ratio)\n    syncThumbToDOM()\n    syncEdgeFades()\n  }, [syncThumbToDOM, syncEdgeFades])\n\n  // Callback ref\n  const scrollRef = useCallback((node: HTMLDivElement | null) => {\n    // Teardown previous\n    if (roRef.current) {\n      roRef.current.disconnect()\n      roRef.current = null\n    }\n    if (moRef.current) {\n      moRef.current.disconnect()\n      moRef.current = null\n    }\n    if (rafIdRef.current) {\n      cancelAnimationFrame(rafIdRef.current)\n      rafIdRef.current = 0\n    }\n\n    scrollElRef.current = node\n\n    if (node) {\n      const ratio = node.clientWidth / node.scrollWidth\n      setThumbRatio(ratio >= 1 ? 0 : ratio)\n\n      const ro = new ResizeObserver(measure)\n      roRef.current = ro\n      const observeAll = () => {\n        ro.disconnect()\n        ro.observe(node)\n        for (const child of Array.from(node.children)) ro.observe(child)\n      }\n      observeAll()\n\n      moRef.current = new MutationObserver(() => {\n        observeAll()\n        measure()\n      })\n      moRef.current.observe(node, { childList: true })\n\n      requestAnimationFrame(() => {\n        syncThumbToDOM()\n        syncEdgeFades()\n      })\n    } else {\n      setThumbRatio(0)\n      prevCanScrollLeftRef.current = false\n      prevCanScrollRightRef.current = false\n      setCanScrollLeft(false)\n      setCanScrollRight(false)\n    }\n  }, [measure, syncThumbToDOM, syncEdgeFades])\n\n  // RAF-throttled scroll handler\n  const onScroll = useCallback(() => {\n    if (isDraggingRef.current) return\n\n    if (rafIdRef.current) cancelAnimationFrame(rafIdRef.current)\n    rafIdRef.current = requestAnimationFrame(() => {\n      syncThumbToDOM()\n      syncEdgeFades()\n    })\n  }, [syncThumbToDOM, syncEdgeFades])\n\n  // Track click — read ratio from DOM, smooth scroll\n  const onTrackClick = useCallback((e: React.MouseEvent) => {\n    if ((e.target as HTMLElement).closest('[data-scrollbar-thumb]')) return\n\n    const track = trackRef.current\n    const el = scrollElRef.current\n    if (!track || !el) return\n\n    const ratio = el.clientWidth / el.scrollWidth\n    if (ratio >= 1) return\n\n    const rect = track.getBoundingClientRect()\n    const thumbWidth = rect.width * ratio\n    const maxThumbOffset = rect.width - thumbWidth\n    if (maxThumbOffset <= 0) return\n\n    const targetFraction = Math.max(0, Math.min(1,\n      (e.clientX - rect.left - thumbWidth / 2) / maxThumbOffset\n    ))\n    const targetScrollLeft = targetFraction * (el.scrollWidth - el.clientWidth)\n    el.scrollTo({ left: targetScrollLeft, behavior: 'smooth' })\n  }, [])\n\n  // Wheel on track → forward to scroll container\n  const onTrackWheel = useCallback((e: React.WheelEvent) => {\n    const el = scrollElRef.current\n    if (!el) return\n    e.preventDefault()\n    el.scrollLeft += e.deltaX || e.deltaY\n  }, [])\n\n  // Drag: pointer down\n  const onThumbPointerDown = useCallback((e: React.PointerEvent) => {\n    e.preventDefault()\n    e.stopPropagation()\n    const el = scrollElRef.current\n    if (!el) return\n    isDraggingRef.current = true\n    dragStartRef.current = { mouseX: e.clientX, scrollLeft: el.scrollLeft }\n    const target = e.currentTarget as HTMLElement\n    target.setPointerCapture(e.pointerId)\n    target.style.cursor = 'grabbing'\n  }, [])\n\n  // Drag: pointer move\n  const onThumbPointerMove = useCallback((e: React.PointerEvent) => {\n    if (!isDraggingRef.current) return\n    const el = scrollElRef.current\n    const track = trackRef.current\n    if (!el || !track) return\n\n    const currentThumbRatio = el.clientWidth / el.scrollWidth\n    if (currentThumbRatio >= 1) return\n\n    const trackWidth = track.clientWidth\n    const thumbWidth = trackWidth * currentThumbRatio\n    const maxThumbTravel = trackWidth - thumbWidth\n    if (maxThumbTravel <= 0) return\n\n    const mouseDelta = e.clientX - dragStartRef.current.mouseX\n    const scrollRange = el.scrollWidth - el.clientWidth\n    el.scrollLeft = Math.min(\n      Math.max(dragStartRef.current.scrollLeft + (mouseDelta / maxThumbTravel) * scrollRange, 0),\n      scrollRange\n    )\n\n    syncThumbToDOM()\n    syncEdgeFades()\n  }, [syncThumbToDOM, syncEdgeFades])\n\n  // Drag: pointer up\n  const onThumbPointerUp = useCallback((e: React.PointerEvent) => {\n    isDraggingRef.current = false\n    const target = e.currentTarget as HTMLElement\n    target.releasePointerCapture(e.pointerId)\n    target.style.cursor = 'grab'\n  }, [])\n\n  return {\n    scrollRef,\n    trackRef,\n    thumbRef,\n    thumbRatio,\n    canScrollLeft,\n    canScrollRight,\n    onScroll,\n    onTrackClick,\n    onTrackWheel,\n    onThumbPointerDown,\n    onThumbPointerMove,\n    onThumbPointerUp,\n  }\n}\n","\"use client\";\n\nimport { useState, useEffect } from 'react';\n\n/**\n * Extract the dominant edge color from an image using color bucketing.\n * Samples pixels along left and right edges (the visible letterbox areas),\n * groups them into color buckets, and returns the most common bucket.\n * This avoids muddy averages when edges have mixed colors (e.g., sky + sand).\n */\nfunction extractEdgeColor(img: HTMLImageElement): string {\n  const canvas = document.createElement('canvas');\n  const ctx = canvas.getContext('2d');\n  if (!ctx) return '#000000';\n\n  const maxSize = 100;\n  const scale = Math.min(maxSize / img.naturalWidth, maxSize / img.naturalHeight);\n  const w = Math.round(img.naturalWidth * scale);\n  const h = Math.round(img.naturalHeight * scale);\n  canvas.width = w;\n  canvas.height = h;\n\n  ctx.drawImage(img, 0, 0, w, h);\n\n  const data = ctx.getImageData(0, 0, w, h).data;\n\n  // Sample left edge, right edge, top edge, bottom edge (15% band)\n  const edgeW = Math.max(2, Math.round(w * 0.15));\n  const edgeH = Math.max(2, Math.round(h * 0.15));\n\n  // Color bucketing: quantize to 32-step buckets for grouping similar colors\n  const bucketSize = 32;\n  const buckets = new Map<string, { r: number; g: number; b: number; count: number }>();\n\n  for (let y = 0; y < h; y++) {\n    for (let x = 0; x < w; x++) {\n      const isEdge =\n        x < edgeW || x >= w - edgeW ||\n        y < edgeH || y >= h - edgeH;\n\n      if (!isEdge) continue;\n\n      const i = (y * w + x) * 4;\n      const a = data[i + 3];\n      if (a < 128) continue;\n\n      const r = data[i];\n      const g = data[i + 1];\n      const b = data[i + 2];\n\n      // Quantize to bucket\n      const br = Math.floor(r / bucketSize) * bucketSize;\n      const bg = Math.floor(g / bucketSize) * bucketSize;\n      const bb = Math.floor(b / bucketSize) * bucketSize;\n      const key = `${br},${bg},${bb}`;\n\n      const existing = buckets.get(key);\n      if (existing) {\n        existing.r += r;\n        existing.g += g;\n        existing.b += b;\n        existing.count++;\n      } else {\n        buckets.set(key, { r, g, b, count: 1 });\n      }\n    }\n  }\n\n  if (buckets.size === 0) return '#000000';\n\n  // Find the most common color bucket\n  let bestBucket: { r: number; g: number; b: number; count: number } | null = null;\n  for (const bucket of buckets.values()) {\n    if (!bestBucket || bucket.count > bestBucket.count) {\n      bestBucket = bucket;\n    }\n  }\n\n  if (!bestBucket || bestBucket.count === 0) return '#000000';\n\n  // Return average color within the winning bucket\n  const r = Math.round(bestBucket.r / bestBucket.count);\n  const g = Math.round(bestBucket.g / bestBucket.count);\n  const b = Math.round(bestBucket.b / bestBucket.count);\n\n  return `rgb(${r}, ${g}, ${b})`;\n}\n\n/**\n * Hook that extracts the dominant edge color from an image URL.\n * Returns a CSS color string for use as a background behind object-contain images.\n *\n * Always sets crossOrigin='anonymous' — required for canvas pixel access.\n * Most CDNs (Supabase, Cloudflare, our image proxy) return CORS headers.\n * If the server doesn't support CORS, onerror fires and we use the fallback.\n *\n * @param imageUrl - URL of the image to analyze\n * @param fallback - Fallback color if extraction fails (default: '#000000')\n * @returns CSS color string (e.g., 'rgb(34, 28, 22)')\n */\nexport function useImageEdgeColor(imageUrl: string | undefined | null, fallback = '#000000'): string {\n  const [color, setColor] = useState(fallback);\n\n  useEffect(() => {\n    if (!imageUrl) {\n      setColor(fallback);\n      return;\n    }\n\n    let cancelled = false;\n    const img = new Image();\n    img.crossOrigin = 'anonymous';\n\n    img.onload = () => {\n      if (cancelled) return;\n      try {\n        setColor(extractEdgeColor(img));\n      } catch {\n        setColor(fallback);\n      }\n    };\n\n    img.onerror = () => {\n      if (cancelled) return;\n      setColor(fallback);\n    };\n\n    img.src = imageUrl;\n\n    return () => { cancelled = true; };\n  }, [imageUrl, fallback]);\n\n  return color;\n}\n","'use client'\n\nimport { useCallback, useEffect, useState } from 'react'\n\nexport interface UseIsTruncatedOptions {\n  /** Measure vertical clipping (`line-clamp-N`) instead of the single-line ellipsis. */\n  multiline?: boolean\n}\n\n/**\n * Tracks whether an element's text is ACTUALLY clipped — the condition for\n * offering a \"full value\" tooltip. Text that fits needs no tooltip, and showing\n * one anyway just repeats what the user is already reading.\n *\n * Three things can change the answer, and all three are watched: the box\n * resizing (`ResizeObserver`), the text itself changing, and the measured\n * ELEMENT changing. None of them implies the others — new text inside a\n * width-constrained box leaves the box exactly as it was, so the observer never\n * fires; and a caller that swaps between render branches (adornment vs plain)\n * mounts a different node under the same text, which a `useRef` would leave\n * measured against the old, detached node. Pass the rendered value as `value`.\n *\n * `ref` is a CALLBACK ref, not a `RefObject` — that is what makes the element\n * itself a dependency. Attach it exactly like a normal ref; there is no\n * `.current` to read.\n *\n * ```tsx\n * const { ref, isTruncated } = useIsTruncated<HTMLHeadingElement>(title)\n * <FloatingTooltip content={title} disabled={!isTruncated}>\n *   <h1 ref={ref} className=\"truncate\">{title}</h1>\n * </FloatingTooltip>\n * ```\n */\nexport function useIsTruncated<T extends HTMLElement>(value: unknown, options?: UseIsTruncatedOptions) {\n  const multiline = options?.multiline ?? false\n  const [element, setElement] = useState<T | null>(null)\n  const [isTruncated, setIsTruncated] = useState(false)\n\n  // Wrapped so the identity stays stable across renders: passing the raw\n  // `useState` setter as a ref would be stable too, but this keeps the public\n  // shape a plain `RefCallback<T>` instead of a state dispatcher.\n  const ref = useCallback((node: T | null) => setElement(node), [])\n\n  useEffect(() => {\n    if (!element) {\n      setIsTruncated(false)\n      return\n    }\n    // The 1px tolerance absorbs sub-pixel layout rounding, which otherwise\n    // reports a permanent fractional \"overflow\" at some zoom levels / DPRs and\n    // leaves a tooltip armed on text that is fully visible.\n    const check = () => {\n      setIsTruncated(\n        multiline\n          ? element.scrollHeight > element.clientHeight + 1\n          : element.scrollWidth > element.clientWidth + 1,\n      )\n    }\n    check()\n    const observer = new ResizeObserver(check)\n    observer.observe(element)\n    return () => observer.disconnect()\n  }, [element, value, multiline])\n\n  return { ref, isTruncated }\n}\n","'use client'\n\nimport { useEffect, useLayoutEffect } from 'react'\n\n/**\n * `useLayoutEffect` in the browser, `useEffect` on the server.\n *\n * `useLayoutEffect` never runs during server rendering — React cannot measure or\n * mutate a layout that has not been laid out — so calling it in a component that\n * SSRs logs a warning on every render pass. Client components still render on the\n * server in Next.js, so `'use client'` alone does not avoid it.\n *\n * Use this wherever the effect writes something the FIRST paint depends on (a CSS\n * custom property, a scroll position, a measured size). A plain `useEffect` there\n * runs after paint, which shows one frame of the pre-effect value; a plain\n * `useLayoutEffect` gets the timing right and pays for it with the warning. This\n * gets both: paint-blocking where there is a paint, a no-op where there is not.\n */\nexport const useIsomorphicLayoutEffect =\n  typeof window !== 'undefined' ? useLayoutEffect : useEffect\n","'use client'\n\nimport { useSyncExternalStore } from 'react'\n\n/**\n * Extra `collisionPadding.bottom` (px) a Radix popper needs to clear the\n * software keyboard. 0 on desktop and anywhere the keyboard is down.\n *\n * Radix positions every popper with floating-ui at `strategy: 'fixed'`, and\n * floating-ui takes its collision viewport from `window.visualViewport`.\n * Neither mobile shell shrinks the box a keyboard opens into: iOS runs\n * Capacitor's Keyboard plugin at `resize: 'none'`, which leaves the WKWebView\n * frame alone, and Android's window is edge-to-edge, where the IME is only a\n * WindowInsets type. So `flip`/`shift`/`size` all believe the screen still\n * reaches the bottom, and a dropdown whose own search field just raised the\n * keyboard is placed straight behind it — worst inside modals, which sit near\n * the bottom of the screen by design.\n *\n * The shell publishes the keyboard height as `--of-keyboard-inset` on <html>\n * (openframe-frontend's `keyboard-inset.ts`; unset everywhere else, hence the\n * 0 default). Handing the part floating-ui does not already know about back as\n * collision padding is what makes the top of the keyboard the bottom edge of\n * the screen.\n *\n * ```tsx\n * const keyboardPadding = useKeyboardCollisionPadding()\n * <PopoverPrimitive.Content collisionPadding={{ bottom: keyboardPadding }} />\n * ```\n *\n * Pair it with `max-h-[var(--radix-popper-available-height)]` on the content:\n * padding alone flips a dropdown above its trigger, but a list taller than what\n * is left above the keyboard would then spill back over it.\n */\n\nconst KEYBOARD_INSET_VAR = '--of-keyboard-inset'\n\nfunction measure(): number {\n  if (typeof document === 'undefined') return 0\n  const inset = Number.parseFloat(\n    getComputedStyle(document.documentElement).getPropertyValue(KEYBOARD_INSET_VAR),\n  )\n  // Also catches the NaN of an unset variable.\n  if (!(inset > 0)) return 0\n  // How far floating-ui's own viewport bottom sits below the top of the\n  // keyboard. iOS reports the keyboard on the visual viewport and Android does\n  // not; measuring the difference rather than branching on the platform keeps\n  // both correct and can never reserve the same keyboard twice.\n  const layoutHeight = document.documentElement.clientHeight\n  const viewport = window.visualViewport\n  const viewportBottom = viewport ? viewport.offsetTop + viewport.height : layoutHeight\n  return Math.max(0, Math.round(viewportBottom - (layoutHeight - inset)))\n}\n\n// One document-wide subscription for every popper on the page: the keyboard is\n// a single global, and each open dropdown re-reading it would mean another\n// MutationObserver plus a forced style resolve per keystroke-driven re-render.\nconst listeners = new Set<() => void>()\nlet padding = 0\nlet teardown: (() => void) | null = null\n\nfunction sync(): void {\n  const next = measure()\n  if (next === padding) return\n  padding = next\n  for (const notify of listeners) notify()\n}\n\nfunction subscribe(onStoreChange: () => void): () => void {\n  listeners.add(onStoreChange)\n  if (listeners.size === 1) {\n    padding = measure()\n    // Two independent signals. The inset lands on <html>'s inline style, so the\n    // attribute is the only notification for it; the visual viewport moves on\n    // its own schedule (iOS reports the keyboard there, and a pinch-zoom shifts\n    // it with no keyboard involved at all) and changes what is left to reserve.\n    const observer = new MutationObserver(sync)\n    observer.observe(document.documentElement, { attributes: true, attributeFilter: ['style'] })\n    const viewport = window.visualViewport\n    viewport?.addEventListener('resize', sync)\n    viewport?.addEventListener('scroll', sync)\n    teardown = () => {\n      observer.disconnect()\n      viewport?.removeEventListener('resize', sync)\n      viewport?.removeEventListener('scroll', sync)\n    }\n  }\n  return () => {\n    listeners.delete(onStoreChange)\n    if (listeners.size > 0) return\n    teardown?.()\n    teardown = null\n  }\n}\n\nexport function useKeyboardCollisionPadding(): number {\n  return useSyncExternalStore(\n    subscribe,\n    () => padding,\n    () => 0,\n  )\n}\n","\"use client\"\n\nimport { useEffect, useRef, useState } from \"react\"\n\n/**\n * Hook to use localStorage with state\n * @param key - localStorage key\n * @param initialValue - Initial value if key doesn't exist\n * @returns [storedValue, setValue] tuple\n */\nexport function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T | ((val: T) => T)) => void] {\n  // State to store our value\n  // Use lazy initialization to read from localStorage synchronously on first render\n  const [storedValue, setStoredValue] = useState<T>(() => {\n    try {\n      if (typeof window !== \"undefined\") {\n        const item = window.localStorage.getItem(key)\n        // Parse stored json or if none return initialValue\n        return item ? JSON.parse(item) : initialValue\n      }\n    } catch (error) {\n      console.error(`Error reading localStorage key \"${key}\":`, error)\n    }\n    return initialValue\n  })\n\n  // Use a ref to track if we've initialized from localStorage\n  const isInitialized = useRef(true) // Set to true since we initialize in useState\n  // Use a ref to track if the current state change came from a storage event\n  const isFromStorageEvent = useRef(false)\n\n  // Listen for storage events to sync with other tabs/components\n  useEffect(() => {\n    if (!isInitialized.current) return\n\n    const handleStorageChange = (e: StorageEvent) => {\n      if (e.key === key && e.newValue !== null) {\n        try {\n          const newValue = JSON.parse(e.newValue)\n          isFromStorageEvent.current = true\n          setStoredValue(newValue)\n          console.log(`🔄 Updated localStorage key \"${key}\" from storage event`)\n        } catch (error) {\n          console.error(`Error parsing storage event for key \"${key}\":`, error)\n        }\n      }\n    }\n\n    const handleCustomStorageUpdate = (e: CustomEvent) => {\n      if (e.detail.key === key) {\n        try {\n          if (e.detail.newValue === null || e.detail.newValue === undefined) {\n            // localStorage was cleared or key was removed\n            isFromStorageEvent.current = true\n            setStoredValue(initialValue)\n            console.log(`🔄 Cleared localStorage key \"${key}\" from custom storage event`)\n          } else {\n            const newValue = JSON.parse(e.detail.newValue)\n            isFromStorageEvent.current = true\n            setStoredValue(newValue)\n            console.log(`🔄 Updated localStorage key \"${key}\" from custom storage event`)\n          }\n        } catch (error) {\n          console.error(`Error parsing custom storage event for key \"${key}\":`, error)\n        }\n      }\n    }\n\n    window.addEventListener('storage', handleStorageChange)\n    window.addEventListener('localStorageUpdate', handleCustomStorageUpdate as EventListener)\n    \n    return () => {\n      window.removeEventListener('storage', handleStorageChange)\n      window.removeEventListener('localStorageUpdate', handleCustomStorageUpdate as EventListener)\n    }\n  }, [key])\n\n  // Update localStorage when state changes, but only after initialization and NOT from storage events\n  useEffect(() => {\n    if (!isInitialized.current) return\n\n    // Skip localStorage write if this state change came from a storage event\n    if (isFromStorageEvent.current) {\n      isFromStorageEvent.current = false\n      return\n    }\n\n    try {\n      // Save state to localStorage\n      if (typeof window !== \"undefined\") {\n        window.localStorage.setItem(key, JSON.stringify(storedValue))\n      }\n    } catch (error) {\n      console.error(`Error writing to localStorage key \"${key}\":`, error)\n    }\n  }, [key, storedValue])\n\n  // Return a wrapped version of useState's setter function that\n  // persists the new value to localStorage\n  const setValue = (value: T | ((val: T) => T)) => {\n    try {\n      // Allow value to be a function so we have same API as useState\n      const valueToStore = value instanceof Function ? value(storedValue) : value\n      // Save state\n      setStoredValue(valueToStore)\n    } catch (error) {\n      console.error(`Error setting value for localStorage key \"${key}\":`, error)\n    }\n  }\n\n  return [storedValue, setValue]\n}\n","'use client'\n\nimport { useCallback, useEffect, useRef, type MutableRefObject } from 'react'\n\n/**\n * THE marquee animation core (single source of truth) — extracted verbatim\n * from <CardsStrip> so every endless-scroll surface (card strips, logo strips,\n * quick-action walls, chip marquees) shares ONE rAF position engine (\"unify\n * means delete variations\").\n *\n * The engine is axis- and driver-agnostic: it owns a FLOAT position ref, a\n * velocity envelope, seam wrapping, and an optional chevron-style glide; the\n * caller decides what a position \"means\" via `apply` — writing\n * `scroller.scrollLeft` (CardsStrip: a real scroller the user can also drag)\n * or a `translate` transform (MarqueeWall: decorative tracks, incl. vertical —\n * mandatory inside surfaces that forbid inner scrollers, e.g. the deck's\n * mobile single-fold panels).\n *\n * Engine semantics (do not \"simplify\"):\n * - Position lives in a FLOAT ref, never read-modify-write from the DOM:\n *   scrollLeft reads back rounded values, so sub-pixel increments\n *   (speed/fps < 1px) get eaten by rounding — stutter + measurably slower\n *   than the configured speed.\n * - Velocity envelope (~250ms time constant) eases between 0 and `speed`\n *   instead of binary stop/start — pause decelerates, resume accelerates from\n *   the current position (GSAP-marquee behavior; hard cuts read as jank).\n * - Glide consumes a signed remaining-distance, wrapping modulo the copy size\n *   each frame, so seam warps are invisible to it (browser smooth-scroll\n *   animates toward an ABSOLUTE target and lunges after a warp).\n * - `isPaused` is evaluated every frame — the caller owns the pause-reason\n *   set (hover, viewport, tab visibility, suppress windows); the engine only\n *   turns it into a smooth velocity target.\n */\nexport interface MarqueeEngineOptions {\n  /** Master switch — the rAF runs only while true. On (re)activation the\n   *  position resyncs from `readBack` when provided. */\n  active: boolean\n  /** Cruise speed in px/s. */\n  speed: number\n  /** Per-frame pause verdict (caller-owned reason set). `now` is the rAF\n   *  timestamp, for suppress-until comparisons. */\n  isPaused: (now: number) => boolean\n  /** Single-copy size in px (the wrap modulus). Return <= 0 to disable\n   *  wrapping (position advances unwrapped — callers should size tracks so\n   *  this never happens while active). */\n  getWrapSize: () => number\n  /** Lower bound of the wrap range: positions stay in\n   *  [wrapMin, wrapMin + wrapSize). Default 0. Scroller drivers with USER\n   *  scrolling pass their seam-warp buffer here so engine-written positions\n   *  never enter the re-center zones — an exact-edge warp check is racy\n   *  against the rAF (the engine nudges the scroller off the edge before the\n   *  scroll handler samples it → \"stuck at the left edge\"). */\n  getWrapMin?: () => number\n  /** Write the position to the DOM (scrollLeft assignment or transform). */\n  apply: (pos: number) => void\n  /** Read the externally-mutated position back (real scrollers: user drag /\n   *  native smooth scroll move scrollLeft under the engine). Omit for\n   *  transform drivers — nothing external can move them. */\n  readBack?: () => number\n  /** Called at the end of every frame (e.g. hover re-resolution after the\n   *  track moved under a stationary pointer). */\n  onAfterFrame?: () => void\n}\n\nexport interface MarqueeEngineHandle {\n  /** The engine's float position (px along the axis, 0 ≤ pos < wrap while\n   *  wrapping). Callers may write it together with their own DOM write (seam\n   *  warp on manual scroll). */\n  posRef: MutableRefObject<number>\n  /** Accumulate a signed glide distance (chevron navigation). Runs through\n   *  the engine's own tween — never native smooth scroll while wrapping. */\n  glideBy: (px: number) => void\n  /** Wrap an arbitrary position into [0, wrapSize). */\n  wrap: (pos: number) => number\n}\n\n// Resync threshold (px): external movement beyond this adopts the DOM value.\nconst RESYNC_EPSILON = 1.5\n\nexport function useMarqueeEngine(options: MarqueeEngineOptions): MarqueeEngineHandle {\n  const posRef = useRef(0)\n  const glideRemainingRef = useRef(0)\n  const speedEnvRef = useRef(0)\n\n  // Latest-options ref: the rAF loop always reads current values without the\n  // effect restarting on every render.\n  const optsRef = useRef(options)\n  optsRef.current = options\n\n  const wrap = useCallback((pos: number) => {\n    const half = optsRef.current.getWrapSize()\n    if (half <= 0) return pos\n    const min = optsRef.current.getWrapMin?.() ?? 0\n    while (pos >= min + half) pos -= half\n    while (pos < min) pos += half\n    return pos\n  }, [])\n\n  const glideBy = useCallback((px: number) => {\n    glideRemainingRef.current += px\n  }, [])\n\n  const active = options.active\n  useEffect(() => {\n    if (!active) return\n    let raf = 0\n    let last = performance.now()\n    const readBack = optsRef.current.readBack\n    if (readBack) posRef.current = readBack()\n\n    const tick = (now: number) => {\n      const opts = optsRef.current\n      const dt = Math.min(now - last, 100) / 1000 // clamp tab-wake jumps\n      last = now\n\n      const paused = opts.isPaused(now)\n      // Ease the velocity toward its target (0 when paused) — smooth decel on\n      // pause, smooth accel on resume, always from the CURRENT position.\n      const targetSpeed = paused ? 0 : opts.speed\n      speedEnvRef.current += (targetSpeed - speedEnvRef.current) * Math.min(1, dt / 0.25)\n      if (targetSpeed === 0 && speedEnvRef.current < 0.5) speedEnvRef.current = 0\n\n      const external = opts.readBack ? opts.readBack() : null\n      const resync = () => {\n        if (external !== null && Math.abs(external - posRef.current) > RESYNC_EPSILON) {\n          posRef.current = external\n        }\n      }\n\n      const glide = glideRemainingRef.current\n      if (glide !== 0) {\n        // Ease-out toward the glide target; runs even while \"paused\" (the\n        // interaction that queued it sets its own suppress window).\n        resync()\n        const speed = Math.max(Math.abs(glide) * 6, 240) // px/s, proportional\n        const step = Math.sign(glide) * Math.min(Math.abs(glide), speed * dt)\n        glideRemainingRef.current = Math.abs(glide - step) < 0.5 ? 0 : glide - step\n        posRef.current = wrap(posRef.current + step)\n        opts.apply(posRef.current)\n      } else if (speedEnvRef.current > 0) {\n        // Resync after external movement (user scroll, seam warp).\n        resync()\n        posRef.current = wrap(posRef.current + speedEnvRef.current * dt)\n        opts.apply(posRef.current)\n      } else if (external !== null) {\n        // Idle: follow external movement so resumes start from where the\n        // user left the scroller.\n        posRef.current = external\n      }\n      opts.onAfterFrame?.()\n      raf = requestAnimationFrame(tick)\n    }\n    raf = requestAnimationFrame(tick)\n    return () => cancelAnimationFrame(raf)\n  }, [active, wrap])\n\n  return { posRef, glideBy, wrap }\n}\n","\"use client\"\n\nimport { useLayoutEffect, useState } from \"react\"\n\n/**\n * Hook to check if a media query matches\n * @param query - Media query to check\n * @returns Whether the media query matches, or undefined during SSR/initial render\n */\nexport function useMediaQuery(\n  query: string,\n): boolean | undefined {\n  const [matches, setMatches] = useState<boolean | undefined>(undefined)\n\n  useLayoutEffect(() => {\n    const matchMedia = window.matchMedia(query)\n\n    const handleChange = () => {\n      setMatches(matchMedia.matches)\n    }\n\n    // Set initial value\n    handleChange()\n\n    matchMedia.addEventListener('change', handleChange)\n\n    return () => {\n      matchMedia.removeEventListener('change', handleChange)\n    }\n  }, [query])\n\n  return matches\n}\n\n/**\n * Predefined breakpoints for common screen sizes\n */\nexport const breakpoints = {\n  md: \"(min-width: 800px)\",   // Tablet: 50rem\n  lg: \"(min-width: 1280px)\",  // Desktop: 80rem\n}\n\n/** @deprecated Use useMdUp instead */\nexport function useSmUp(): boolean | undefined {\n  return useMdUp()\n}\n\nexport function useMdUp(): boolean | undefined {\n  const matches = useMediaQuery(breakpoints.md)\n  return matches === undefined ? undefined : matches\n}\n\nexport function useLgUp(): boolean | undefined {\n  const matches = useMediaQuery(breakpoints.lg)\n  return matches === undefined ? undefined : matches\n}\n\n","\"use client\"\n\nimport { useCallback, useRef } from \"react\"\n\n/**\n * Hook to memoize a callback with dependencies\n * Similar to useCallback but with deep comparison of dependencies\n * @param callback - Function to memoize\n * @param dependencies - Dependencies array\n * @returns Memoized callback\n */\nexport function useMemoizedCallback<T extends (...args: any[]) => any>(callback: T, dependencies: any[]): T {\n  // Store the callback and dependencies\n  const callbackRef = useRef<T>(callback)\n  const dependenciesRef = useRef<any[]>(dependencies)\n\n  // Update the callback if it changes\n  callbackRef.current = callback\n\n  // Check if dependencies have changed\n  const depsChanged = dependencies.some((dep, i) => !Object.is(dep, dependenciesRef.current[i]))\n\n  // Update dependencies if they've changed\n  if (depsChanged) {\n    dependenciesRef.current = dependencies\n  }\n\n  // Return memoized callback\n  return useCallback(((...args: any[]) => callbackRef.current(...args)) as T, [depsChanged])\n}\n","'use client'\n\nimport { useCallback, useEffect, useState } from 'react'\n\nexport interface UseNotificationPermissionResult {\n  /** Whether the page-context Notification API exists in this browser (false during SSR and on iOS Safari). */\n  supported: boolean\n  permission: NotificationPermission\n  /** Prompt the user for permission. Must be called from a user gesture or browsers will auto-deny. */\n  request: () => Promise<NotificationPermission>\n}\n\n/**\n * Tracks the browser's desktop-notification permission, staying in sync when\n * the user changes the site setting externally (Permissions API change event\n * where available, visibility/focus re-read as the Safari fallback).\n */\nexport function useNotificationPermission(): UseNotificationPermissionResult {\n  const [supported, setSupported] = useState(false)\n  const [permission, setPermission] = useState<NotificationPermission>('default')\n\n  useEffect(() => {\n    if (typeof window === 'undefined' || !('Notification' in window)) return\n    setSupported(true)\n    const sync = () => setPermission(Notification.permission)\n    sync()\n\n    // The query resolves async; without the flag it would attach the listener after unmount.\n    let cancelled = false\n    let status: PermissionStatus | undefined\n    navigator.permissions\n      ?.query({ name: 'notifications' as PermissionName })\n      .then((s) => {\n        if (cancelled) return\n        status = s\n        s.addEventListener('change', sync)\n      })\n      .catch(() => undefined)\n\n    document.addEventListener('visibilitychange', sync)\n    window.addEventListener('focus', sync)\n    return () => {\n      cancelled = true\n      status?.removeEventListener('change', sync)\n      document.removeEventListener('visibilitychange', sync)\n      window.removeEventListener('focus', sync)\n    }\n  }, [])\n\n  const request = useCallback(async () => {\n    if (typeof window === 'undefined' || !('Notification' in window)) {\n      return 'denied' as NotificationPermission\n    }\n    // Older Safari only implements the callback form; resolving from both is harmless.\n    const result = await new Promise<NotificationPermission>((resolve) => {\n      const maybePromise = Notification.requestPermission(resolve)\n      if (maybePromise && typeof maybePromise.then === 'function') void maybePromise.then(resolve)\n    })\n    setPermission(Notification.permission)\n    return result\n  }, [])\n\n  return { supported, permission, request }\n}\n","'use client'\n\nimport { useState, useEffect, useCallback } from 'react'\nimport {\n  saveOnboardingState,\n  loadOnboardingState,\n  markStepComplete as storageMarkComplete,\n  markStepSkipped as storageMarkSkipped,\n  dismissOnboarding as storageDismiss,\n  markMultipleComplete as storageMarkMultiple,\n  isStepComplete as storageIsComplete,\n  isStepSkipped as storageIsSkipped,\n  type OnboardingState\n} from '../../utils/onboarding-storage'\n\nexport interface OnboardingStepConfig {\n  id: string\n  title: string\n  description: string\n  actionIcon: (color?: string) => React.ReactNode\n  actionText: string\n  completedText: string\n  onAction: () => void | Promise<void>\n  onSkip?: () => void\n  checkComplete?: () => boolean | Promise<boolean>\n}\n\nexport type { OnboardingState }\n\n/**\n * Hook for managing onboarding state with localStorage persistence\n * Uses simple storage utilities for atomic updates and reliable re-renders\n */\nexport function useOnboardingState(storageKey: string = 'openframe-onboarding-state') {\n  const [state, setState] = useState<OnboardingState>(() => loadOnboardingState(storageKey))\n  const [, forceUpdate] = useState(0)\n\n  // Listen for storage changes from other tabs\n  useEffect(() => {\n    const handleStorageUpdate = (e: CustomEvent) => {\n      if (e.detail.key === storageKey) {\n        const newState = loadOnboardingState(storageKey)\n        setState(newState)\n        forceUpdate(prev => prev + 1)\n        console.log('🔄 State updated from storage event:', newState)\n      }\n    }\n\n    window.addEventListener('localStorageUpdate', handleStorageUpdate as EventListener)\n    return () => {\n      window.removeEventListener('localStorageUpdate', handleStorageUpdate as EventListener)\n    }\n  }, [storageKey])\n\n  const markComplete = useCallback((stepId: string) => {\n    console.log(`🎯 markComplete called for: \"${stepId}\"`)\n    const newState = storageMarkComplete(storageKey, stepId)\n    setState(newState)\n    forceUpdate(prev => prev + 1)\n  }, [storageKey])\n\n  const markSkipped = useCallback((stepId: string) => {\n    console.log(`⏭️ markSkipped called for: \"${stepId}\"`)\n    const newState = storageMarkSkipped(storageKey, stepId)\n    setState(newState)\n    forceUpdate(prev => prev + 1)\n  }, [storageKey])\n\n  const dismissOnboarding = useCallback(() => {\n    console.log(`🚫 dismissOnboarding called`)\n    const newState = storageDismiss(storageKey)\n    setState(newState)\n    forceUpdate(prev => prev + 1)\n  }, [storageKey])\n\n  const markMultipleComplete = useCallback((stepIds: string[]) => {\n    console.log(`🎯 markMultipleComplete called for:`, stepIds)\n    const newState = storageMarkMultiple(storageKey, stepIds)\n    setState(newState)\n    forceUpdate(prev => prev + 1)\n    console.log(`📝 State after batch:`, newState)\n  }, [storageKey])\n\n  const isStepComplete = useCallback((stepId: string): boolean => {\n    return state.completedSteps.includes(stepId)\n  }, [state.completedSteps])\n\n  const isStepSkipped = useCallback((stepId: string): boolean => {\n    return state.skippedSteps.includes(stepId)\n  }, [state.skippedSteps])\n\n  const allStepsComplete = useCallback((steps: OnboardingStepConfig[]): boolean => {\n    return steps.every(step => isStepComplete(step.id) || isStepSkipped(step.id))\n  }, [isStepComplete, isStepSkipped])\n\n  return {\n    state,\n    markComplete,\n    markSkipped,\n    dismissOnboarding,\n    isStepComplete,\n    isStepSkipped,\n    allStepsComplete,\n    markMultipleComplete\n  }\n}\n","/**\n * Onboarding state storage utilities\n * Simple localStorage wrapper following announcement-storage.ts pattern\n * Provides atomic writes to prevent race conditions\n */\n\nexport interface OnboardingState {\n  completedSteps: string[]\n  skippedSteps: string[]\n  dismissed: boolean\n  lastUpdated: string\n}\n\nconst DEFAULT_STATE: OnboardingState = {\n  completedSteps: [],\n  skippedSteps: [],\n  dismissed: false,\n  lastUpdated: new Date().toISOString()\n}\n\n/**\n * Save onboarding state to localStorage (atomic write)\n */\nexport function saveOnboardingState(key: string, state: OnboardingState): void {\n  if (typeof window === 'undefined') return\n\n  try {\n    localStorage.setItem(key, JSON.stringify(state))\n    console.log('💾 Saved onboarding state:', state)\n\n    // Dispatch custom event for cross-tab sync\n    if (typeof window !== 'undefined') {\n      window.dispatchEvent(\n        new CustomEvent('localStorageUpdate', {\n          detail: { key, newValue: JSON.stringify(state) }\n        })\n      )\n    }\n  } catch (err) {\n    console.warn('[onboarding-storage] Failed saving to localStorage:', err)\n  }\n}\n\n/**\n * Load onboarding state from localStorage\n */\nexport function loadOnboardingState(key: string): OnboardingState {\n  if (typeof window === 'undefined') return DEFAULT_STATE\n\n  try {\n    const raw = localStorage.getItem(key)\n    if (!raw) return DEFAULT_STATE\n\n    const parsed = JSON.parse(raw) as OnboardingState\n    return parsed\n  } catch (err) {\n    console.warn('[onboarding-storage] Failed parsing localStorage data:', err)\n    return DEFAULT_STATE\n  }\n}\n\n/**\n * Mark multiple steps as complete in a single atomic update\n */\nexport function markMultipleComplete(\n  key: string,\n  stepIds: string[]\n): OnboardingState {\n  const state = loadOnboardingState(key)\n  const newState: OnboardingState = {\n    ...state,\n    completedSteps: [...new Set([...state.completedSteps, ...stepIds])],\n    skippedSteps: state.skippedSteps.filter(id => !stepIds.includes(id)),\n    lastUpdated: new Date().toISOString()\n  }\n  saveOnboardingState(key, newState)\n  return newState\n}\n\n/**\n * Mark a single step as complete\n */\nexport function markStepComplete(key: string, stepId: string): OnboardingState {\n  return markMultipleComplete(key, [stepId])\n}\n\n/**\n * Mark a step as skipped\n */\nexport function markStepSkipped(key: string, stepId: string): OnboardingState {\n  const state = loadOnboardingState(key)\n  const newState: OnboardingState = {\n    ...state,\n    skippedSteps: [...new Set([...state.skippedSteps, stepId])],\n    completedSteps: state.completedSteps.filter(id => id !== stepId),\n    lastUpdated: new Date().toISOString()\n  }\n  saveOnboardingState(key, newState)\n  return newState\n}\n\n/**\n * Dismiss the onboarding walkthrough\n */\nexport function dismissOnboarding(key: string): OnboardingState {\n  const state = loadOnboardingState(key)\n  const newState: OnboardingState = {\n    ...state,\n    dismissed: true,\n    lastUpdated: new Date().toISOString()\n  }\n  saveOnboardingState(key, newState)\n  return newState\n}\n\n/**\n * Check if a step is complete\n */\nexport function isStepComplete(key: string, stepId: string): boolean {\n  const state = loadOnboardingState(key)\n  return state.completedSteps.includes(stepId)\n}\n\n/**\n * Check if a step is skipped\n */\nexport function isStepSkipped(key: string, stepId: string): boolean {\n  const state = loadOnboardingState(key)\n  return state.skippedSteps.includes(stepId)\n}\n\n/**\n * Check if onboarding is dismissed\n */\nexport function isOnboardingDismissed(key: string): boolean {\n  const state = loadOnboardingState(key)\n  return state.dismissed\n}\n","import { useEffect } from 'react'\n\n/**\n * Runs `reset` whenever the tab is hidden or the window loses focus — exactly the\n * moments a `pointerleave` / `pointercancel` can be SWALLOWED (tab blur, an\n * overlay mounting under the cursor, an interrupted pointer), which otherwise\n * leaves a hover/\"inside\" flag stuck `true` and freezes a marquee FOREVER. The\n * next real pointer/hover re-arms the flag.\n *\n * SSOT self-heal for every marquee surface (MarqueeWall, CardsStrip) so the\n * blur + `visibilitychange` wiring lives in one place. `reset` almost always\n * only UN-sticks state. The one edge: on `blur` WITHOUT the tab hiding (a second\n * window focused beside this one), clearing the hover-pause can let the marquee\n * resume under a still-stationary cursor in the now-unfocused window — it\n * re-pauses on the next `pointermove`/`pointerenter`. We keep the `blur` reset\n * anyway because it's the only signal that catches a tab-blur that swallows the\n * `pointerleave` without a `visibilitychange`. Pass a STABLE `reset` (memoize\n * with `useCallback`) — the listeners re-attach whenever its identity changes.\n */\nexport function useResetOnPageHidden(reset: () => void): void {\n  useEffect(() => {\n    const onVisibility = () => {\n      if (document.visibilityState === 'hidden') reset()\n    }\n    window.addEventListener('blur', reset)\n    document.addEventListener('visibilitychange', onVisibility)\n    return () => {\n      window.removeEventListener('blur', reset)\n      document.removeEventListener('visibilitychange', onVisibility)\n    }\n  }, [reset])\n}\n","\"use client\"\n\nimport { useState, useEffect, useCallback } from \"react\"\nimport { useDebounce } from \"./use-debounce\"\nimport type { SearchResult } from \"../../components/ui/search-input\"\n\nexport interface UseSearchConfig<T> {\n  /** Async function that performs the search */\n  searchFn: (query: string) => Promise<T[]>\n  /** Maps each raw item to a SearchResult */\n  mapResult: (item: T) => SearchResult\n  /** Debounce delay in ms. Default 300 */\n  debounceMs?: number\n  /** Minimum characters before searching. Default 2 */\n  minQueryLength?: number\n}\n\nexport interface UseSearchReturn {\n  query: string\n  setQuery: (q: string) => void\n  results: SearchResult[]\n  isLoading: boolean\n  error: string | null\n  clearResults: () => void\n}\n\n/**\n * Generic search state management hook.\n *\n * Debounces the query, calls `searchFn` when the debounced value meets\n * `minQueryLength`, and maps the raw results via `mapResult`.\n */\nexport function useSearch<T>(config: UseSearchConfig<T>): UseSearchReturn {\n  const { searchFn, mapResult, debounceMs = 300, minQueryLength = 2 } = config\n\n  const [query, setQuery] = useState(\"\")\n  const [results, setResults] = useState<SearchResult[]>([])\n  const [isLoading, setIsLoading] = useState(false)\n  const [error, setError] = useState<string | null>(null)\n\n  const debouncedQuery = useDebounce(query, debounceMs)\n\n  const clearResults = useCallback(() => {\n    setResults([])\n    setError(null)\n  }, [])\n\n  useEffect(() => {\n    // Clear when query is empty or below threshold\n    if (!debouncedQuery || debouncedQuery.length < minQueryLength) {\n      setResults([])\n      setIsLoading(false)\n      setError(null)\n      return\n    }\n\n    let cancelled = false\n\n    const run = async () => {\n      setIsLoading(true)\n      setError(null)\n\n      try {\n        const rawResults = await searchFn(debouncedQuery)\n\n        if (!cancelled) {\n          setResults(rawResults.map(mapResult))\n        }\n      } catch (err) {\n        if (!cancelled) {\n          setError(err instanceof Error ? err.message : \"Search failed\")\n          setResults([])\n        }\n      } finally {\n        if (!cancelled) {\n          setIsLoading(false)\n        }\n      }\n    }\n\n    run()\n\n    return () => {\n      cancelled = true\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [debouncedQuery, minQueryLength])\n\n  return { query, setQuery, results, isLoading, error, clearResults }\n}\n","'use client'\n\nimport { useEffect, useRef } from 'react'\n\n// Every focusable a clone might contain — matched so it can be pulled out of the\n// keyboard tab order (see useSuppressCloneFocus). `[contenteditable]:not(\n// [contenteditable=\"false\"])` catches the empty/`\"true\"`/`\"plaintext-only\"`\n// editable forms; `summary` is focusable inside a <details>.\nconst CLONE_FOCUSABLE_SELECTOR =\n  'a[href],button,input,select,textarea,iframe,summary,audio[controls],video[controls],[tabindex],[contenteditable]:not([contenteditable=\"false\"])'\n\n/**\n * Keep a marquee clone's content pointer-CLICKABLE while taking it out of the\n * accessibility tree and the keyboard tab order. Shared by every endless-loop\n * surface that renders a second track copy (<CardsStrip> managed cells,\n * <MarqueeWall> clone copies).\n *\n * `inert` (the previous approach) does all three at once: a11y-hidden,\n * un-focusable AND un-clickable. That third effect was the \"news marquee stops\n * being clickable after you scroll it for a while\" bug — the 2-copy endless\n * loop ALWAYS paints clone cards inside the viewport near the copy seam, so a\n * user who manually scrolls and parks there is looking at real-looking cards\n * that silently swallow every click. Clones must therefore stay clickable (a\n * click on ANY visible card must open its link — mouse, ⌘-click, middle-click\n * and context-menu all handled natively by the clone's own anchor).\n *\n * The a11y contract is still met without `inert`: the caller keeps\n * `aria-hidden` on the clone wrapper (out of the a11y tree) and this hook\n * forces every focusable descendant to `tabindex=\"-1\"` (out of the tab order →\n * no duplicate tab stops, no aria-hidden-focus violation). Re-applied on\n * subtree mutation. This is the standard loop-clone treatment used by\n * Swiper/Splide.\n */\nexport function useSuppressCloneFocus(active: boolean) {\n  const ref = useRef<HTMLDivElement | null>(null)\n  useEffect(() => {\n    const root = ref.current\n    if (!root || !active) return\n    const suppress = () => {\n      root.querySelectorAll<HTMLElement>(CLONE_FOCUSABLE_SELECTOR).forEach(el => {\n        // Only stamp the ones not already stamped — avoids touching the DOM (and\n        // clobbering an intentional tabindex) on every mutation.\n        if (el.getAttribute('tabindex') !== '-1') el.setAttribute('tabindex', '-1')\n      })\n    }\n    suppress()\n    // Watch structure AND the attributes that can turn a descendant tabbable\n    // (an anchor gaining `href`, React restoring `tabindex=\"0\"`, an element\n    // becoming editable, media gaining `controls`). The `!== '-1'` guard above\n    // makes our own tabindex writes a no-op, so the observer never loops.\n    const mo = new MutationObserver(suppress)\n    mo.observe(root, {\n      childList: true,\n      subtree: true,\n      attributes: true,\n      attributeFilter: ['href', 'tabindex', 'contenteditable', 'controls'],\n    })\n    return () => mo.disconnect()\n  }, [active])\n  return ref\n}\n","'use client'\n\nimport { useMemo } from 'react'\nimport type { CursorPaginationProps } from '../../components/ui/cursor-pagination'\n\n/**\n * Client-side pagination configuration\n */\nexport interface ClientPaginationConfig {\n  type: 'client'\n  currentPage: number\n  totalPages: number\n  itemCount: number\n  itemName?: string\n  onNext: () => void\n  onPrevious: () => void\n  showInfo?: boolean\n}\n\n/**\n * Server-side cursor pagination configuration\n */\nexport interface ServerPaginationConfig {\n  type: 'server'\n  hasNextPage: boolean\n  hasLoadedBeyondFirst: boolean\n  startCursor?: string | null\n  endCursor?: string | null\n  itemCount: number\n  itemName?: string\n  onNext: () => void | Promise<void>\n  onReset: () => void | Promise<void>\n  showInfo?: boolean\n}\n\nexport type PaginationConfig = ClientPaginationConfig | ServerPaginationConfig\n\n/**\n * Unified pagination hook that works for both client-side and server-side pagination\n *\n * @example Client-side pagination\n * ```typescript\n * const pagination = useTablePagination({\n *   type: 'client',\n *   currentPage: 1,\n *   totalPages: 10,\n *   itemCount: 20,\n *   itemName: 'scripts',\n *   onNext: () => setPage(p => p + 1),\n *   onPrevious: () => setPage(p => p - 1),\n *   onReset: () => setPage(1)\n * })\n * ```\n *\n * @example Server-side pagination\n * ```typescript\n * const pagination = useTablePagination({\n *   type: 'server',\n *   hasNextPage: pageInfo.hasNextPage,\n *   hasLoadedBeyondFirst: true,\n *   startCursor: pageInfo.startCursor,\n *   endCursor: pageInfo.endCursor,\n *   itemCount: devices.length,\n *   itemName: 'devices',\n *   onNext: fetchNextPage,\n *   onReset: fetchFirstPage\n * })\n * ```\n */\nexport function useTablePagination(\n  config: PaginationConfig | null\n): CursorPaginationProps | undefined {\n  return useMemo(() => {\n    if (!config) return undefined\n\n    if (config.type === 'client') {\n      // Client-side pagination: show if more than 1 page\n      if (config.totalPages <= 1) return undefined\n\n      return {\n        hasNextPage: config.currentPage < config.totalPages,\n        hasPreviousPage: config.currentPage > 1,\n        isFirstPage: config.currentPage === 1,\n        startCursor: (config.currentPage - 1).toString(),\n        endCursor: config.currentPage.toString(),\n        currentCount: config.itemCount,\n        itemName: config.itemName || 'items',\n        onNext: () => config.onNext(),\n        onPrevious: () => config.onPrevious(),\n        showInfo: config.showInfo ?? true\n      }\n    } else {\n      // Server-side pagination: show if pageInfo exists\n      return {\n        hasNextPage: config.hasNextPage,\n        hasPreviousPage: config.hasLoadedBeyondFirst,\n        isFirstPage: !config.hasLoadedBeyondFirst,\n        startCursor: config.startCursor,\n        endCursor: config.endCursor,\n        currentCount: config.itemCount,\n        itemName: config.itemName || 'items',\n        onNext: config.onNext,\n        onPrevious: config.onReset, // Previous button goes back to first page\n        showInfo: config.showInfo ?? true\n      }\n    }\n  }, [config])\n}\n","\"use client\"\n\nimport { useState, useEffect, useRef } from \"react\"\n\n/**\n * Hook to throttle a value\n * @param value - Value to throttle\n * @param limit - Throttle limit in milliseconds\n * @returns Throttled value\n */\nexport function useThrottle<T>(value: T, limit = 200): T {\n  const [throttledValue, setThrottledValue] = useState<T>(value)\n  const lastUpdated = useRef<number>(Date.now())\n\n  useEffect(() => {\n    const now = Date.now()\n    const elapsed = now - lastUpdated.current\n\n    // If enough time has elapsed, update the throttled value\n    if (elapsed >= limit) {\n      setThrottledValue(value)\n      lastUpdated.current = now\n    } else {\n      // Otherwise, set up a timeout to update after the limit\n      const timerId = setTimeout(() => {\n        setThrottledValue(value)\n        lastUpdated.current = Date.now()\n      }, limit - elapsed)\n\n      return () => {\n        clearTimeout(timerId)\n      }\n    }\n  }, [value, limit])\n\n  return throttledValue\n}\n","import { useLayoutEffect, useState } from \"react\"\n\n/**\n * Hook to get window dimensions\n * @returns Window width and height\n */\nexport function useWindowSize() {\n  const [windowSize, setWindowSize] = useState({\n    width: 0,\n    height: 0,\n  })\n  const [isClient, setIsClient] = useState(false)\n\n  useLayoutEffect(() => {\n    setIsClient(true)\n    if (!isClient) return\n\n    const handleResize = () => {\n      setWindowSize({\n        width: window.innerWidth,\n        height: window.innerHeight,\n      })\n    }\n\n    // Set initial size\n    handleResize()\n\n    // Add event listener\n    window.addEventListener(\"resize\", handleResize)\n\n    // Remove event listener on cleanup\n    return () => window.removeEventListener(\"resize\", handleResize)\n  }, [isClient])\n\n  return windowSize\n}","// UI Hooks exports\nexport * from './use-auto-limit-tags'\nexport * from './use-debounce'\nexport * from './use-deferred-error'\nexport * from './use-focus-trap'\nexport * from './use-header-height'\nexport * from './use-horizontal-scrollbar'\nexport * from './use-image-edge-color'\nexport * from './use-is-truncated'\nexport * from './use-isomorphic-layout-effect'\nexport * from './use-keyboard-collision-padding'\nexport * from './use-local-storage'\nexport * from './use-marquee-engine'\nexport * from './use-media-query'\nexport * from './use-memoized-callback'\nexport * from './use-notification-permission'\nexport * from './use-onboarding-state'\nexport * from './use-reset-on-page-hidden'\nexport * from './use-search'\nexport * from './use-suppress-clone-focus'\nexport * from './use-table-pagination'\nexport * from './use-throttle'\nexport * from './use-window-size'\n\n// Shared ref-counted, iOS-aware body scroll lock (react-aria). Re-exported so\n// hub/app consumers have one import path; lib internals may import the\n// package directly (npm dedupes to one module instance either way).\nexport { usePreventScroll } from '@react-aria/overlays'\n","import { useState, useEffect } from 'react';\nimport type { PlatformConfig, PlatformOption } from '../../types/platform';\nimport { transformPlatformConfigsToOptions } from '../../utils/platform-config';\nimport type { SelectableOption } from '../../components/features';\n\nexport interface UsePlatformConfigResult {\n  platforms: PlatformConfig[];\n  platformOptions: PlatformOption[];\n  selectableOptions: SelectableOption[];  // Rich options with icons and colors\n  isLoading: boolean;\n  error: Error | null;\n}\n\n// Cache for platform configs to avoid repeated fetches\nlet platformCache: PlatformConfig[] | null = null;\nlet fetchPromise: Promise<PlatformConfig[]> | null = null;\n\n/**\n * Custom hook to fetch platform configurations from API\n * Provides both full platform configs and simplified options for dropdowns\n * Heavily cached to prevent excessive API calls - should only call once per session\n * \n * NOTE: This hook is designed to work without react-query dependency\n */\nexport function usePlatformConfig(): UsePlatformConfigResult {\n  const [platforms, setPlatforms] = useState<PlatformConfig[]>(platformCache || []);\n  const [isLoading, setIsLoading] = useState(!platformCache);\n  const [error, setError] = useState<Error | null>(null);\n\n  useEffect(() => {\n    // If we already have cached platforms, use them\n    if (platformCache) {\n      setPlatforms(platformCache);\n      setIsLoading(false);\n      return;\n    }\n\n    // If a fetch is already in progress, wait for it\n    if (fetchPromise) {\n      fetchPromise\n        .then(data => {\n          setPlatforms(data);\n          setIsLoading(false);\n        })\n        .catch(err => {\n          setError(err);\n          setIsLoading(false);\n        });\n      return;\n    }\n\n    // Start a new fetch\n    console.log('🔧 Fetching platform configurations from API (should only happen once)');\n    \n    fetchPromise = fetch('/api/config/platforms')\n      .then(response => {\n        if (!response.ok) {\n          throw new Error(`Failed to fetch platform config: ${response.statusText}`);\n        }\n        return response.json();\n      })\n      .then(data => {\n        const platforms = data.platforms || data;\n        console.log('✅ Platform configurations loaded:', platforms.length, 'platforms');\n        platformCache = platforms;\n        fetchPromise = null;\n        return platforms;\n      });\n\n    fetchPromise\n      .then(data => {\n        setPlatforms(data);\n        setIsLoading(false);\n      })\n      .catch(err => {\n        console.error('❌ Failed to fetch platform config:', err);\n        setError(err);\n        setIsLoading(false);\n        fetchPromise = null;\n      });\n  }, []);\n  \n  // Create options for dropdowns with \"All Platforms\" option\n  const platformOptions: PlatformOption[] = [\n    { value: 'all', label: 'All Platforms' },\n    ...platforms.map((platform: PlatformConfig) => ({\n      value: platform.value,\n      label: platform.label\n    }))\n  ];\n\n  // Create rich selectable options with icons and colors\n  const selectableOptions = transformPlatformConfigsToOptions(platforms);\n\n  return {\n    platforms,\n    platformOptions,\n    selectableOptions,\n    isLoading,\n    error\n  };\n}\n\n/**\n * Get platform configuration by value\n */\nexport function usePlatformByValue(value: string): PlatformConfig | undefined {\n  const { platforms } = usePlatformConfig();\n  return platforms.find(platform => platform.value === value);\n}\n\n/**\n * Check if a platform value is valid\n */\nexport function useValidatePlatform(value: string): boolean {\n  const { platforms } = usePlatformConfig();\n  return platforms.some(platform => platform.value === value);\n} ","import React from 'react';\nimport { OpenmspLogo, FlamingoLogo, OpenFrameLogo, MiamiCyberGangLogoFaceOnly } from '../components/icons';\nimport { Globe } from 'lucide-react';\nimport type { SelectableOption } from '../components/features';\nimport type { PlatformConfig } from '../types/platform';\n\n// Platform icons mapping with consistent colors matching app theme\nexport const platformIcons = {\n  openframe: <OpenFrameLogo className=\"h-5 w-5\" lowerPathColor=\"#FFC008\" upperPathColor=\"#ffffff\" />,\n  openmsp: <OpenmspLogo className=\"h-5 w-5\" />,\n  flamingo: <FlamingoLogo className=\"h-5 w-5\" fill=\"#EC4899\" />,\n  'flamingo-teaser': <FlamingoLogo className=\"h-5 w-5\" fill=\"#EC4899\" />,\n  'marketing-hub': <FlamingoLogo className=\"h-5 w-5\" fill=\"#F357BB\" />,\n  'product-hub': <FlamingoLogo className=\"h-5 w-5\" fill=\"#5EA62E\" />,\n  'revenue-hub': <FlamingoLogo className=\"h-5 w-5\" fill=\"#FFC008\" />,\n  'people-hub': <FlamingoLogo className=\"h-5 w-5\" fill=\"#5EFAF0\" />,\n  'company-hub': <FlamingoLogo className=\"h-5 w-5\" fill=\"#f36666\" />,\n  tmcg: <MiamiCyberGangLogoFaceOnly  className=\"h-5 w-5\" />,\n  universal: <Globe className=\"h-5 w-5 text-[#10B981]\" />\n};\n\n// Platform colors mapping\nexport const platformColors = {\n  openmsp: 'bg-[#3B82F6]',\n  openframe: 'bg-[#8B5CF6]',\n  flamingo: 'bg-[#EC4899]',\n  'flamingo-teaser': 'bg-[#F59E0B]',\n  'marketing-hub': 'bg-[#F357BB]',\n  'product-hub': 'bg-[#5EA62E]',\n  'revenue-hub': 'bg-[#FFC008]',\n  'people-hub': 'bg-[#5EFAF0]',\n  'company-hub': 'bg-[#f36666]',\n  tmcg: 'bg-[#FF6B6B]',\n  universal: 'bg-[#10B981]'\n};\n\n// Platform display names for consistent naming across the app\nexport const platformDisplayNames = {\n  openmsp: 'OpenMSP',\n  openframe: 'OpenFrame',\n  flamingo: 'Flamingo',\n  'flamingo-teaser': 'Flamingo Teaser',\n  'marketing-hub': 'Flamingo Marketing Hub',\n  'product-hub': 'Flamingo Product Hub',\n  'revenue-hub': 'Flamingo Revenue Hub',\n  'people-hub': 'Flamingo People Hub',\n  'company-hub': 'Flamingo Company Hub',\n  tmcg: 'TMCG',\n  universal: 'Universal'\n};\n\n// Platform descriptions for consistent messaging across the app\nexport const platformDescriptions = {\n  openmsp: 'Comprehensive directory and comparison platform for managed service providers (MSPs) and technology vendors. Reduce vendor costs and discover open-source alternatives.',\n  openframe: 'AI-driven open-source security operations center (SOC) and endpoint detection platform for MSPs.',\n  flamingo: 'AI-driven open-source OS for MSPs. Swap bloated vendor tools for open ones. Automate the boring crap. Take your margin back.',\n  'flamingo-teaser': 'Preview of Flamingo - the AI-driven open-source OS for MSPs.',\n  tmcg: 'The Miami Cyber Gang - A cybersecurity community focused on education and collaboration.',\n  universal: 'Cross-platform universal content.'\n};\n\n// Platform slogans for branding consistency\nexport const platformSlogans = {\n  openmsp: 'Find Your Perfect MSP Partner',\n  openframe: 'Open-Source Security Operations',\n  flamingo: 'Open-Source OS for MSPs',\n  'flamingo-teaser': 'Coming Soon: Open-Source OS for MSPs',\n  tmcg: 'Miami Cyber Community',\n  universal: 'Universal Platform'\n};\n\n// Platform hex colors for default configuration\nexport const platformHexColors = {\n  openmsp: '#FFC008',\n  openframe: '#FFC008',\n  flamingo: '#FF6B9D',\n  universal: '#FFC008',\n  'flamingo-teaser': '#F59E0B',\n  'marketing-hub': '#F357BB',\n  'product-hub': '#5EA62E',\n  'revenue-hub': '#FFC008',\n  'people-hub': '#5EFAF0',\n  'company-hub': '#f36666',\n  tmcg: '#FF6B6B'\n};\n\n// Platform icon names for default configuration\nexport const platformIconNames = {\n  openmsp: 'openmsp-logo',\n  openframe: 'openframe-logo',\n  flamingo: 'flamingo-logo',\n  universal: 'globe',\n  'flamingo-teaser': 'flamingo-logo',\n  'marketing-hub': 'flamingo-logo',\n  'product-hub': 'flamingo-logo',\n  'revenue-hub': 'flamingo-logo',\n  'people-hub': 'flamingo-logo',\n  'company-hub': 'flamingo-logo',\n  tmcg: 'tmcg-logo'\n};\n\n/**\n * Get default color for platform\n */\nexport function getDefaultColorForPlatform(platformName: string): string {\n  return platformHexColors[platformName as keyof typeof platformHexColors] || platformHexColors.universal;\n}\n\n/**\n * Get default icon name for platform\n */\nexport function getDefaultIconForPlatform(platformName: string): string {\n  return platformIconNames[platformName as keyof typeof platformIconNames] || platformIconNames.universal;\n}\n\nexport function transformPlatformConfigsToOptions(platformConfigs: PlatformConfig[]): SelectableOption[] {\n  return platformConfigs.map((platform: PlatformConfig) => ({\n    id: platform.id,                    // Database UUID for matching\n    name: platform.name,                // Platform name enum\n    displayName: platform.display_name, // Human-readable name\n    description: platform.description,\n    icon: platformIcons[platform.name as keyof typeof platformIcons] || platformIcons.universal,\n    color: platformColors[platform.name as keyof typeof platformColors] || platformColors.universal\n  }));\n}\n\n/**\n * Get platform icon by name\n */\nexport function getPlatformIcon(platformName: string) {\n  return platformIcons[platformName as keyof typeof platformIcons] || platformIcons.universal;\n}\n\n/**\n * Get platform color by name\n */\nexport function getPlatformColor(platformName: string) {\n  return platformColors[platformName as keyof typeof platformColors] || platformColors.universal;\n}\n\n/**\n * Get platform display name by name\n */\nexport function getPlatformDisplayName(platformName: string): string {\n  return platformDisplayNames[platformName as keyof typeof platformDisplayNames] || platformName;\n}\n\n/**\n * Get platform description by name\n */\nexport function getPlatformDescription(platformName: string): string {\n  return platformDescriptions[platformName as keyof typeof platformDescriptions] || platformName;\n}\n\n/**\n * Get platform slogan by name\n */\nexport function getPlatformSlogan(platformName: string): string {\n  return platformSlogans[platformName as keyof typeof platformSlogans] || platformName;\n}\n\n/**\n * Get small platform icon for filter buttons with white colors (4x4 size)\n */\nexport function getSmallPlatformIcon(platformName: string): React.ReactNode {\n  const className = \"h-4 w-4 flex-shrink-0\";\n\n  switch (platformName) {\n    case 'openframe':\n      return <OpenFrameLogo className={className} lowerPathColor=\"#FFC008\" upperPathColor=\"#ffffff\" />;\n    case 'openmsp':\n      return <OpenmspLogo className={className} frontBubbleColor=\"#f1f1f1\" innerFrontBubbleColor=\"#000000\" backBubbleColor=\"#FFC008\" />;\n    case 'flamingo':\n    case 'flamingo-teaser':\n      return <FlamingoLogo className={`${className}`} fill=\"#EC4899\" />;\n    case 'marketing-hub':\n      return <FlamingoLogo className={className} fill=\"var(--ods-flamingo-pink-base)\" />;\n    case 'product-hub':\n      return <FlamingoLogo className={className} fill=\"var(--ods-attention-green-success)\" />;\n    case 'revenue-hub':\n      return <FlamingoLogo className={className} fill=\"var(--ods-attention-yellow-warning)\" />;\n    case 'people-hub':\n      return <FlamingoLogo className={className} fill=\"var(--ods-flamingo-cyan-base)\" />;\n    case 'company-hub':\n      return <FlamingoLogo className={className} fill=\"var(--ods-attention-red-error)\" />;\n    case 'tmcg':\n      return <MiamiCyberGangLogoFaceOnly className={className} />;\n    case 'universal':\n    default:\n      return <Globe className={className} />;\n  }\n}\n\n/**\n * Get platform icon for admin/selector components (standard 6x6 size)\n */\nexport function getPlatformIconComponent(platformName: string, className: string = \"h-6 w-6\"): React.ReactNode {\n  switch (platformName) {\n    case 'openframe':\n      return <OpenFrameLogo className={className} />;\n    case 'openmsp':\n      // `OpenmspLogo` ignores `color`; it uses the three bubble props. On a dark\n      // surface the default black front-bubble vanishes — use the canonical dark\n      // treatment (matches getPlatformIcon above + every hub call-site).\n      return <OpenmspLogo className={className} frontBubbleColor=\"#f1f1f1\" innerFrontBubbleColor=\"#000000\" backBubbleColor=\"#FFC008\" />;\n    case 'flamingo':\n    case 'flamingo-teaser':\n      return <FlamingoLogo className={`${className} text-white`} />;\n    case 'marketing-hub':\n      return <FlamingoLogo className={className} fill=\"var(--ods-flamingo-pink-base)\" />;\n    case 'product-hub':\n      return <FlamingoLogo className={className} fill=\"var(--ods-attention-green-success)\" />;\n    case 'revenue-hub':\n      return <FlamingoLogo className={className} fill=\"var(--ods-attention-yellow-warning)\" />;\n    case 'people-hub':\n      return <FlamingoLogo className={className} fill=\"var(--ods-flamingo-cyan-base)\" />;\n    case 'company-hub':\n      return <FlamingoLogo className={className} fill=\"var(--ods-attention-red-error)\" />;\n    case 'tmcg':\n      return <MiamiCyberGangLogoFaceOnly size={24} className={className} />;\n    case 'universal':\n    default:\n      return <Globe className={className} />;\n  }\n}","import { toast as sonnerToast } from 'sonner'\nimport { showToast, type ToastVariant } from '../components/ui/toaster'\n\nexport interface ToastOptions {\n  title?: string\n  description?: string\n  variant?: 'default' | 'success' | 'destructive' | 'info' | 'warning' | 'error'\n  duration?: number\n  dismissible?: boolean\n}\n\nconst normalizeVariant = (variant: ToastOptions['variant']): ToastVariant => {\n  if (variant === 'destructive') return 'error'\n  if (!variant) return 'default'\n  return variant\n}\n\nexport const toast = (options: ToastOptions | string) => {\n  if (typeof options === 'string') {\n    return showToast({ title: options })\n  }\n\n  const { title, description, variant, duration, dismissible } = options\n  return showToast({\n    title,\n    description,\n    variant: normalizeVariant(variant),\n    duration,\n    dismissible,\n  })\n}\n\nexport const useToast = () => ({\n  toast,\n  dismiss: sonnerToast.dismiss,\n  promise: sonnerToast.promise,\n})\n","'use client'\n\nimport * as React from 'react'\nimport { Toaster as SonnerToaster, toast as sonnerToast } from 'sonner'\nimport type { ExternalToast } from 'sonner'\nimport { Chevron02DownIcon } from '../icons-v2-generated/arrows/chevron-02-down-icon'\nimport { XmarkIcon } from '../icons-v2-generated/signs-and-symbols/xmark-icon'\nimport { ToolIcon } from '../tool-icon'\nimport type { ToolType } from '../../types/tool.types'\nimport { cn } from '../../utils/cn'\n\nexport type ToastVariant = 'default' | 'success' | 'warning' | 'error' | 'info'\n\nexport const dotColorByVariant: Record<ToastVariant, string> = {\n  default: 'bg-ods-text-secondary',\n  success: 'bg-ods-success',\n  warning: 'bg-ods-warning',\n  error: 'bg-ods-error',\n  info: 'bg-ods-text-secondary',\n}\n\nexport const progressColorByVariant: Record<ToastVariant, string> = {\n  default: 'bg-ods-text-secondary',\n  success: 'bg-ods-success',\n  warning: 'bg-ods-warning',\n  error: 'bg-ods-error',\n  info: 'bg-ods-text-secondary',\n}\n\ninterface ToastHeaderProps {\n  id: string | number\n  variant: ToastVariant\n  title?: React.ReactNode\n  description?: React.ReactNode\n  duration?: number\n  dismissible?: boolean\n  className?: string\n  showProgress?: boolean\n}\n\nfunction ToastHeader({\n  id,\n  variant,\n  title,\n  description,\n  duration = 4000,\n  dismissible = true,\n  className,\n  showProgress = true,\n}: ToastHeaderProps) {\n  return (\n    <div\n      className={cn(\n        'relative flex w-full items-start gap-2 overflow-hidden bg-ods-card p-3',\n        className,\n      )}\n    >\n      <div className=\"flex size-6 shrink-0 items-center justify-center\">\n        <span className={cn('size-[9px] rounded-full', dotColorByVariant[variant])} />\n      </div>\n\n      <div className=\"flex min-w-0 flex-1 flex-col justify-center\">\n        {title ? (\n          <p className=\"truncate pr-5 text-h4 text-ods-text-primary\" title={typeof title === 'string' ? title : undefined}>{title}</p>\n        ) : null}\n        {description ? (\n          <p className=\"text-h6 text-ods-text-secondary line-clamp-3\" title={typeof description === 'string' ? description : undefined}>{description}</p>\n        ) : null}\n      </div>\n\n      {dismissible ? (\n        <button\n          type=\"button\"\n          aria-label=\"Close\"\n          onClick={() => sonnerToast.dismiss(id)}\n          className=\"absolute right-[7px] top-[7px] flex size-4 items-center justify-center text-ods-text-secondary transition-colors hover:text-ods-text-primary\"\n        >\n          <XmarkIcon size={16} />\n        </button>\n      ) : null}\n\n      {showProgress && duration !== Infinity && duration > 0 ? (\n        <div\n          className={cn(\n            'absolute inset-x-0 bottom-0 h-1 origin-left',\n            progressColorByVariant[variant],\n          )}\n          style={{\n            animation: `toast-progress ${duration}ms linear forwards`,\n          }}\n        />\n      ) : null}\n    </div>\n  )\n}\n\nexport interface ToastCardProps {\n  id: string | number\n  variant?: ToastVariant\n  title?: React.ReactNode\n  description?: React.ReactNode\n  duration?: number\n  dismissible?: boolean\n  className?: string\n}\n\nexport function ToastCard({\n  id,\n  variant = 'default',\n  title,\n  description,\n  duration = 4000,\n  dismissible = true,\n  className,\n}: ToastCardProps) {\n  return (\n    <div\n      role=\"status\"\n      className={cn(\n        'w-[368px] max-w-[calc(100vw-32px)] overflow-hidden rounded-md border border-ods-border bg-ods-card shadow-lg',\n        className,\n      )}\n    >\n      <ToastHeader\n        id={id}\n        variant={variant}\n        title={title}\n        description={description}\n        duration={duration}\n        dismissible={dismissible}\n      />\n    </div>\n  )\n}\n\nexport interface CommandApprovalToastProps {\n  id: string | number\n  variant?: ToastVariant\n  title?: React.ReactNode\n  description?: React.ReactNode\n  command: string\n  toolType?: ToolType\n  approvalDescription?: React.ReactNode\n  approveLabel?: string\n  rejectLabel?: string\n  onApprove?: () => void\n  onReject?: () => void\n  duration?: number\n  dismissible?: boolean\n  defaultExpanded?: boolean\n  className?: string\n}\n\nexport function CommandApprovalToast({\n  id,\n  variant = 'warning',\n  title = 'Tech Required',\n  description = 'Approval is required to execute the command.',\n  command,\n  toolType,\n  approvalDescription,\n  approveLabel = 'Approve',\n  rejectLabel = 'Reject',\n  onApprove,\n  onReject,\n  duration = Infinity,\n  dismissible = true,\n  defaultExpanded = false,\n  className,\n}: CommandApprovalToastProps) {\n  const [expanded, setExpanded] = React.useState(defaultExpanded)\n\n  const handleApprove = () => {\n    onApprove?.()\n    sonnerToast.dismiss(id)\n  }\n\n  const handleReject = () => {\n    onReject?.()\n    sonnerToast.dismiss(id)\n  }\n\n  return (\n    <div\n      role=\"status\"\n      className={cn(\n        'flex w-[368px] max-w-[calc(100vw-32px)] flex-col overflow-hidden rounded-md border border-ods-border bg-ods-bg shadow-lg',\n        className,\n      )}\n    >\n      <ToastHeader\n        id={id}\n        variant={variant}\n        title={title}\n        description={description}\n        duration={duration}\n        dismissible={dismissible}\n        showProgress={!expanded}\n        className=\"border-b border-ods-border\"\n      />\n\n      <div\n        className=\"grid transition-[grid-template-rows] duration-300 ease-out\"\n        style={{ gridTemplateRows: expanded ? '1fr' : '0fr' }}\n        aria-hidden={!expanded}\n      >\n        <div className=\"overflow-hidden\">\n          <div className=\"flex h-11 w-full items-center gap-2 border-b border-ods-border bg-ods-card px-3 py-2\">\n            <p className=\"min-w-0 flex-1 truncate text-h6 text-ods-text-primary\" title={command}>\n              {command}\n            </p>\n            {toolType ? <ToolIcon toolType={toolType} size={16} /> : null}\n          </div>\n\n          <div className=\"flex flex-col gap-2 bg-ods-bg p-3\">\n            {approvalDescription ? (\n              <p className=\"text-h6 text-ods-text-secondary\">\n                {approvalDescription}\n              </p>\n            ) : null}\n            <div className=\"flex items-center gap-4\">\n              <button\n                type=\"button\"\n                onClick={handleApprove}\n                tabIndex={expanded ? 0 : -1}\n                className=\"flex flex-1 items-center justify-center rounded-md bg-ods-accent px-2 py-2 text-h5 text-ods-text-on-accent transition-colors hover:bg-ods-accent-hover active:bg-ods-accent-active\"\n              >\n                {approveLabel}\n              </button>\n              <button\n                type=\"button\"\n                onClick={handleReject}\n                tabIndex={expanded ? 0 : -1}\n                className=\"flex flex-1 items-center justify-center rounded-md border border-ods-border bg-ods-card px-2 py-2 text-h5 text-ods-text-primary transition-colors hover:bg-ods-bg-hover\"\n              >\n                {rejectLabel}\n              </button>\n            </div>\n          </div>\n        </div>\n      </div>\n\n      {expanded ? null : (\n        <button\n          type=\"button\"\n          onClick={() => setExpanded(true)}\n          className=\"flex w-full items-center gap-2 bg-ods-card px-3 py-2 text-left text-h6 text-ods-text-primary transition-colors hover:bg-ods-bg-hover\"\n          aria-expanded={false}\n        >\n          <span className=\"flex-1\">Show Command</span>\n          <Chevron02DownIcon size={16} />\n        </button>\n      )}\n    </div>\n  )\n}\n\nexport type ToasterProps = React.ComponentProps<typeof SonnerToaster>\n\nexport function Toaster({\n  position = 'bottom-right',\n  offset = 24,\n  gap = 8,\n  toastOptions,\n  ...rest\n}: ToasterProps = {}) {\n  const { classNames: userClassNames, ...restToastOptions } = toastOptions ?? {}\n\n  return (\n    <>\n      <style>{`\n        @keyframes toast-progress {\n          from { transform: scaleX(1); }\n          to { transform: scaleX(0); }\n        }\n      `}</style>\n      <SonnerToaster\n        position={position}\n        offset={offset}\n        gap={gap}\n        toastOptions={{\n          unstyled: true,\n          ...restToastOptions,\n          classNames: {\n            toast: 'w-full',\n            ...userClassNames,\n          },\n        }}\n        {...rest}\n      />\n    </>\n  )\n}\n\nexport interface ShowToastOptions extends Omit<ExternalToast, 'description'> {\n  title?: React.ReactNode\n  description?: React.ReactNode\n  variant?: ToastVariant\n}\n\nexport function showToast(options: ShowToastOptions | string) {\n  const opts: ShowToastOptions =\n    typeof options === 'string' ? { title: options } : options\n\n  const {\n    title,\n    description,\n    variant = 'default',\n    duration = 4000,\n    dismissible = true,\n    ...rest\n  } = opts\n\n  return sonnerToast.custom(\n    (id) => (\n      <ToastCard\n        id={id}\n        variant={variant}\n        title={title}\n        description={description}\n        duration={duration}\n        dismissible={dismissible}\n      />\n    ),\n    { duration, dismissible, ...rest },\n  )\n}\n\nexport interface ShowCommandApprovalToastOptions\n  extends Omit<ExternalToast, 'description'>,\n    Omit<CommandApprovalToastProps, 'id'> {}\n\nexport function showCommandApprovalToast(options: ShowCommandApprovalToastOptions) {\n  const {\n    variant,\n    title,\n    description,\n    command,\n    toolType,\n    approvalDescription,\n    approveLabel,\n    rejectLabel,\n    onApprove,\n    onReject,\n    defaultExpanded,\n    duration = Infinity,\n    dismissible = true,\n    ...rest\n  } = options\n\n  return sonnerToast.custom(\n    (id) => (\n      <CommandApprovalToast\n        id={id}\n        variant={variant}\n        title={title}\n        description={description}\n        command={command}\n        toolType={toolType}\n        approvalDescription={approvalDescription}\n        approveLabel={approveLabel}\n        rejectLabel={rejectLabel}\n        onApprove={onApprove}\n        onReject={onReject}\n        defaultExpanded={defaultExpanded}\n        duration={duration}\n        dismissible={dismissible}\n      />\n    ),\n    { duration, dismissible, ...rest },\n  )\n}\n","/**\n * Centralized Tool Types\n *\n * Single source of truth for all tool-related types across the entire platform.\n * Used by ToolBadge, ToolIcon, and any component that needs tool type information.\n */\n\nexport const ToolTypeValues = {\n  FLEET_MDM: 'FLEET_MDM',\n  MESHCENTRAL: 'MESHCENTRAL',\n  AUTHENTIK: 'AUTHENTIK',\n  OPENFRAME: 'OPENFRAME',\n  OPENFRAME_CHAT: 'OPENFRAME_CHAT',\n  OPENFRAME_CLIENT: 'OPENFRAME_CLIENT',\n  OPENFRAME_RMM: 'OPENFRAME_RMM',\n  OSQUERY: 'OSQUERY',\n  SYSTEM: 'SYSTEM'\n} as const\n\nexport type ToolType = (typeof ToolTypeValues)[keyof typeof ToolTypeValues]\n\n/**\n * Maps tool types to display labels\n */\nexport const toolLabels: Record<ToolType, string> = {\n  FLEET_MDM: 'Fleet',\n  MESHCENTRAL: 'MeshCentral',\n  AUTHENTIK: 'Authentik',\n  OPENFRAME: 'OpenFrame',\n  OPENFRAME_CHAT: 'OpenFrame Chat',\n  OPENFRAME_CLIENT: 'OpenFrame Client',\n  OPENFRAME_RMM: 'RMM',\n  OSQUERY: 'Osquery',\n  SYSTEM: 'System'\n}\n","import * as React from \"react\";\nimport { ToolType, ToolTypeValues } from \"../types/tool.types\";\nimport { OpenFrameLogo } from \"./icons\";\nimport {\n\tOsqueryLogoGreyIcon,\n\tMeshcentralLogoGreyIcon,\n\tFleetMdmLogoGreyIcon,\n\tAuthentikLogoGreyIcon,\n} from \"./icons-v2-generated\";\n\nconst renderOpenFrameLogo = (_size: number, className?: string) => (\n\t// eslint-disable-next-line deprecation/deprecation\n\t<OpenFrameLogo\n\t\tclassName={className ?? \"h-4 w-auto\"}\n\t\tlowerPathColor=\"var(--color-accent-primary)\"\n\t\tupperPathColor=\"var(--color-text-primary)\"\n\t/>\n);\n\nconst toolIconMap: Record<ToolType, (size: number, className?: string) => React.ReactNode> = {\n\t[ToolTypeValues.FLEET_MDM]: (size, className) => <FleetMdmLogoGreyIcon size={size} className={className} />,\n\t[ToolTypeValues.MESHCENTRAL]: (size, className) => <MeshcentralLogoGreyIcon size={size} className={className} />,\n\t[ToolTypeValues.OPENFRAME]: renderOpenFrameLogo,\n\t[ToolTypeValues.OPENFRAME_CHAT]: renderOpenFrameLogo,\n\t[ToolTypeValues.OPENFRAME_CLIENT]: renderOpenFrameLogo,\n\t[ToolTypeValues.OPENFRAME_RMM]: renderOpenFrameLogo,\n\t[ToolTypeValues.AUTHENTIK]: (size, className) => <AuthentikLogoGreyIcon size={size} className={className} />,\n\t[ToolTypeValues.OSQUERY]: (size, className) => <OsqueryLogoGreyIcon size={size} className={className} />,\n\t[ToolTypeValues.SYSTEM]: () => null,\n};\n\nexport interface ToolIconProps {\n\ttoolType: ToolType;\n\tsize?: number;\n\tclassName?: string;\n}\n\nexport const ToolIcon: React.FC<ToolIconProps> = ({ toolType, size = 16, className }) =>\n\t<>{toolIconMap[toolType]?.(size, className) ?? null}</>;\n\nToolIcon.displayName = \"ToolIcon\";\n","import { useState, useCallback, useEffect } from 'react';\nimport { useToast } from \"./use-toast\";\nimport { useRouter } from '../embed-shims/next-navigation';\nimport { useRequiredEndpointsRuntime } from '../contexts/endpoints-runtime-context';\nimport { contentFetch } from '../utils/embed-content-fetch';\n\ninterface ContactSubmissionOptions {\n  userId?: string;\n  successRedirectUrl?: string;\n  successToastMessage?: string;\n  onSuccess?: () => void;\n}\n\n/**\n * Mirrors the API contract at POST /api/contact (see `ContactBaseSchema` in\n * the consuming app). Keep optional fields aligned with the Zod schema —\n * anything missing here OR there will be silently dropped on submission.\n *\n * Tracking fields (rdt_cid, utm_*, referrer_url) are forwarded as-is and\n * persisted by the API; they don't need to be declared on the form, but\n * the index signature below preserves any extra keys callers spread in.\n */\ninterface ContactFormData {\n  name: string;\n  email: string;\n  helpCategory: string;\n  message: string;\n  linkedin_url?: string;\n  companySize?: string;\n  referralSource?: string;\n  rdt_cid?: string;\n  // Permissive index signature — tracking metadata, A/B variants, and\n  // future per-form fields flow through unchanged. The API decides what\n  // to keep via its Zod schema.\n  [key: string]: unknown;\n}\n\n/**\n * useContactSubmission\n * --------------------\n * Provides a helper for submitting contact form data to `/api/contact`.\n * Handles loading state, success detection, toast notifications, and redirect.\n * Follows the same pattern as useWaitlistRegistration for consistency.\n */\nexport function useContactSubmission(options: ContactSubmissionOptions = {}) {\n  const { userId, successRedirectUrl, successToastMessage, onSuccess } = options;\n  const { toast } = useToast();\n  const router = useRouter();\n  // Endpoint URL injected via context — hub provides hub default, embedded\n  // app provides its proxied path. Throws if no provider is mounted.\n  const { contactUrl } = useRequiredEndpointsRuntime();\n  const [isSubmitting, setIsSubmitting] = useState(false);\n  const [isSuccess, setIsSuccess] = useState(false);\n\n  const submit = useCallback(async (formData: ContactFormData) => {\n    if (isSubmitting) return;\n\n    setIsSubmitting(true);\n\n    try {\n      const response = await contentFetch(contactUrl, {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify({ \n          ...formData,\n          user_id: userId\n        }),\n      });\n\n      const data = await response.json();\n\n      if (!response.ok) {\n        throw new Error(data.error || 'Failed to submit contact form');\n      }\n\n      // Success\n      setIsSuccess(true);\n      const message = successToastMessage \n        ? `Thank you! Your message has been sent. ${successToastMessage}`\n        : 'Thank you! Your message has been sent successfully.';\n      \n      toast({\n        title: \"Message sent!\",\n        description: message,\n        variant: 'success',\n      });\n    } catch (error) {\n      const message = error instanceof Error ? error.message : 'Something went wrong. Please try again.';\n      toast({ \n        title: 'Failed to send message', \n        description: message, \n        variant: 'destructive' \n      });\n      throw error; // allow caller to handle if needed\n    } finally {\n      setIsSubmitting(false);\n    }\n  }, [isSubmitting, toast, userId, successToastMessage, contactUrl]);\n\n  // Handle redirect after success\n  useEffect(() => {\n    if (isSuccess && successRedirectUrl) {\n      console.log('🚀 Contact submission successful, redirecting to:', successRedirectUrl);\n      const timer = setTimeout(() => {\n        console.log('🎯 Performing redirect now to:', successRedirectUrl);\n        if (successRedirectUrl.startsWith('http')) {\n          window.location.href = successRedirectUrl;\n        } else {\n          router.push(successRedirectUrl);\n        }\n      }, 1500);\n      \n      return () => clearTimeout(timer);\n    }\n  }, [isSuccess, successRedirectUrl, router]);\n\n  // Call onSuccess callback if provided\n  useEffect(() => {\n    if (isSuccess && onSuccess) {\n      onSuccess();\n    }\n  }, [isSuccess, onSuccess]);\n\n  return { submit, isSubmitting, isSuccess, setIsSuccess } as const;\n}","/**\n * Thin JSON-typed Web-Storage adapter (localStorage or sessionStorage).\n *\n * Centralizes the SSR-guard + try/catch + silent quota-failure pattern\n * that every per-feature storage util would otherwise re-implement.\n *\n * Optional `namespace` prefix supports platform / user partitioning —\n * the resolver runs lazily at call time so the namespace can vary across\n * the lifetime of the page (e.g. proxy-auth switches user, which switches\n * the key suffix).\n *\n * Backend selection (`backend: 'local' | 'session'`):\n *   - `'local'` (default): persists across browser sessions. Use for\n *     UI state, chat history metadata, feature-flag opt-ins.\n *   - `'session'`: cleared when the tab closes. Use for ANY auth-adjacent\n *     value (bearer tokens, act-as identity, proxy credentials). Reduces\n *     the XSS-exfiltration attack window from \"indefinite\" to \"until tab\n *     close\" without losing per-session ergonomics.\n */\n\nexport type WebStorageBackend = 'local' | 'session'\n\nexport interface LocalStorageAdapter<T> {\n  load(): T | null\n  save(value: T): void\n  clear(): void\n  /** Resolved storage key for the current call. Useful for tests. */\n  resolveKey(): string\n}\n\nexport interface LocalStorageAdapterOptions<T> {\n  /** Base storage key. Combined with `namespace()` when provided. */\n  key: string\n  /** Optional dynamic namespace prefix appended via `.` separator.\n   *  Called on EVERY read/write so the key can vary across the page\n   *  lifetime (e.g. when the platform or user identity changes). */\n  namespace?: () => string | null | undefined\n  /** Runtime shape check. Falsey return → `load()` yields null. */\n  validate?: (parsed: unknown) => parsed is T\n  /** Diagnostic prefix written to `console.warn` on parse / write\n   *  failures. Defaults to `'[local-storage]'`. */\n  logTag?: string\n  /** Which Web-Storage backend to use. Defaults to `'local'`. Pass\n   *  `'session'` for anything auth-adjacent so the value evaporates\n   *  when the tab closes. */\n  backend?: WebStorageBackend\n}\n\nfunction getStorage(backend: WebStorageBackend): Storage | null {\n  if (typeof window === 'undefined') return null\n  try {\n    return backend === 'session' ? window.sessionStorage : window.localStorage\n  } catch {\n    // Some sandboxed contexts (Safari private mode older versions,\n    // strict CSP) throw on storage access — treat as unavailable.\n    return null\n  }\n}\n\nexport function createLocalStorageAdapter<T>(\n  options: LocalStorageAdapterOptions<T>,\n): LocalStorageAdapter<T> {\n  const tag = options.logTag ?? '[local-storage]'\n  const backend: WebStorageBackend = options.backend ?? 'local'\n  const resolveKey = (): string => {\n    const ns = options.namespace?.()\n    return ns ? `${ns}.${options.key}` : options.key\n  }\n\n  return {\n    resolveKey,\n    load() {\n      const storage = getStorage(backend)\n      if (!storage) return null\n      try {\n        const raw = storage.getItem(resolveKey())\n        if (!raw) return null\n        const parsed = JSON.parse(raw) as unknown\n        if (options.validate && !options.validate(parsed)) return null\n        return parsed as T\n      } catch (err) {\n        console.warn(`${tag} parse failed for key ${resolveKey()}:`, err)\n        return null\n      }\n    },\n    save(value: T) {\n      const storage = getStorage(backend)\n      if (!storage) return\n      try {\n        storage.setItem(resolveKey(), JSON.stringify(value))\n      } catch (err) {\n        console.warn(`${tag} write failed for key ${resolveKey()}:`, err)\n      }\n    },\n    clear() {\n      const storage = getStorage(backend)\n      if (!storage) return\n      try {\n        storage.removeItem(resolveKey())\n      } catch (err) {\n        console.warn(`${tag} clear failed for key ${resolveKey()}:`, err)\n      }\n    },\n  }\n}\n","// Stub app config\nexport const APP_CONFIG = {\n  app: {\n    type: 'openmsp',\n    name: 'OpenMSP',\n    domain: 'openmsp.ai'\n  },\n  features: {\n    announcements: true,\n    notifications: true\n  }\n} as const;\n\nexport function getAppConfig() {\n  return APP_CONFIG;\n}\n\nexport function getAppType() {\n  // The exact `process.env.NEXT_PUBLIC_APP_TYPE` member expression is kept so\n  // Next/webpack can statically inline it. The try/catch makes the call safe\n  // in non-Next React hosts (Vite/CRA embeds) where `process` does not exist\n  // at runtime — those hosts get the 'openmsp' fallback, which is fine for\n  // embeds: dismissal cookies are domain-scoped and an embed domain serves\n  // one platform's announcements.\n  try {\n    return process.env.NEXT_PUBLIC_APP_TYPE || 'openmsp';\n  } catch {\n    return 'openmsp';\n  }\n}","'use client'\n\n/**\n * Client-side persistence for embed-surface proxy credentials (a\n * PLATFORM API KEY + impersonation email). Used by every embedded\n * surface — the chat widget AND the ticket center AND any future\n * embedded React component that needs to identify itself as the\n * impersonated customer.\n *\n * When set, the surface attaches the creds as\n *   `Authorization: Bearer <platform API key>` + `X-Chat-Act-As: <email>`\n * on every call to `/api/docs/chat`, `/api/chat/*`, and any other route\n * gated by `requireChatAuth` — proving to the server that this session\n * is acting on behalf of <email>. The key is minted in the hub's\n * `/admin/api-keys` and is platform-bound (`fpk_<platform>_…`): a\n * SERVICE key may act as any email; a PERSONAL key only as its owner.\n *\n * **Naming history:** the stored field is still named `secret` and the\n * wire-side header names are `X-Chat-*`. Those are client/server\n * contracts predating the per-platform key migration (the old shared\n * `CHAT_PROXY_SECRET` env is removed hub-side); renaming them would\n * break persisted state + the wire shape for zero behavior change. The\n * CLIENT-side helpers were renamed `Embed*` so non-chat surfaces (e.g.\n * ticket center) don't have to import a chat-prefixed symbol just to\n * send the same headers.\n *\n * Persists to **`localStorage`** so the bearer token + act-as identity\n * survive tab close, new-tab opens, and browser restarts — the\n * `/debug` paste-creds UI is an admin tool and re-pasting every tab\n * cycle was rejected as a dev-experience tradeoff that wasn't worth\n * the security gain. An XSS sink on this origin can read the value\n * indefinitely (vs only-this-tab with sessionStorage), but `/debug`\n * is admin-gated behind the platform's `askAI.enabled` flag, the key\n * is server-verified (hash-at-rest, platform-bound, revocable in\n * `/admin/api-keys`), and the act-as reach is bounded by the key's\n * tier. Explicit \"Clear\" button on the creds bar is the supported\n * logout path; closing the tab is no longer.\n *\n * Namespaced under `<platform>.chat.proxy-auth.v1` (the storage key is\n * unchanged from the old chat-prefixed helper — that's a storage\n * contract; renaming it would log everyone out).\n */\n\nimport { createLocalStorageAdapter } from './local-storage-adapter'\nimport { getAppType } from './app-config'\n\nexport interface EmbedProxyAuth {\n  secret: string\n  email: string\n  /** Optional identity passthrough — empty/omitted = not sent. Server\n   *  parses these as `X-Chat-{First,Last}-Name` / `X-Chat-Avatar-Url` and\n   *  threads them through `resolveChatProxyIdentity`'s returned user. */\n  firstName?: string\n  lastName?: string\n  avatarUrl?: string\n}\n\nfunction isValidPersistedAuth(value: unknown): value is EmbedProxyAuth {\n  if (!value || typeof value !== 'object') return false\n  const v = value as Record<string, unknown>\n  if (\n    typeof v.secret !== 'string' || v.secret.trim().length === 0 ||\n    typeof v.email !== 'string' || v.email.trim().length === 0\n  ) return false\n  // Optional fields: when present must be strings. Empty string is treated\n  // as absent later (in `getEmbedProxyAuth`).\n  if (v.firstName != null && typeof v.firstName !== 'string') return false\n  if (v.lastName != null && typeof v.lastName !== 'string') return false\n  if (v.avatarUrl != null && typeof v.avatarUrl !== 'string') return false\n  return true\n}\n\nconst adapter = createLocalStorageAdapter<EmbedProxyAuth>({\n  // Storage key unchanged from the legacy chat-prefixed helper. Renaming\n  // it would silently log every existing admin out — the key is a\n  // storage contract, not a code identifier.\n  key: 'chat.proxy-auth.v1',\n  namespace: () => getAppType(),\n  validate: isValidPersistedAuth,\n  logTag: '[embed-proxy-auth-storage]',\n  // localStorage — survives tab close, new tabs, and browser restarts.\n  // Admin re-pasting creds every tab cycle was the dev-experience\n  // tradeoff prior `sessionStorage` setup demanded — rejected. See\n  // file-level doc comment for the security tradeoff rationale.\n  backend: 'local',\n})\n\n/** Trim + null-coerce an optional identity field so consumers can do\n *  `auth.firstName ?? ''` without worrying about whitespace-only strings. */\nfunction normalizeOptional(value: string | undefined): string | undefined {\n  if (!value) return undefined\n  const trimmed = value.trim()\n  return trimmed.length > 0 ? trimmed : undefined\n}\n\n/**\n * Returns full credentials (secret + email + optional identity passthrough)\n * when secret + email are available. Returns `null` when nothing is saved —\n * callers treat that as \"fall back to cookie auth\".\n */\nexport function getEmbedProxyAuth(): EmbedProxyAuth | null {\n  const persisted = adapter.load()\n  if (!persisted) return null\n  return {\n    secret: persisted.secret,\n    email: persisted.email.trim().toLowerCase(),\n    firstName: normalizeOptional(persisted.firstName),\n    lastName: normalizeOptional(persisted.lastName),\n    avatarUrl: normalizeOptional(persisted.avatarUrl),\n  }\n}\n\n/**\n * Returns the LAST email the admin saved. The proxy creds bar reads\n * this to pre-fill the email field on mount.\n */\nexport function getPersistedProxyEmail(): string | null {\n  const persisted = adapter.load()\n  return persisted?.email.trim().toLowerCase() ?? null\n}\n\n/** Save the proxy creds. Secret + email are required; identity-passthrough\n *  fields are persisted only when non-empty. */\nexport function setEmbedProxyAuth(value: EmbedProxyAuth): void {\n  adapter.save({\n    secret: value.secret,\n    email: value.email.trim().toLowerCase(),\n    firstName: normalizeOptional(value.firstName),\n    lastName: normalizeOptional(value.lastName),\n    avatarUrl: normalizeOptional(value.avatarUrl),\n  })\n}\n\n/** Drop the persisted creds. */\nexport function clearEmbedProxyAuth(): void {\n  adapter.clear()\n}\n\n/**\n * Apply the embed-proxy auth (Bearer + X-Chat-Act-As) to a fetch call's\n * URL + headers. Used by every embedded-surface route that needs to\n * identify itself as the proxied customer (chat stream, agent-* routes,\n * ticket-center actions). When proxy auth is absent (regular\n * cookie-session users), returns the inputs unchanged so the cookie-auth\n * path still works.\n *\n * `X-Chat-Act-As` header (vs a URL query param) keeps PII out of access\n * logs, Sentry breadcrumbs, browser history, and CDN analytics.\n */\nexport function applyProxyAuth(\n  url: string,\n  baseHeaders: Record<string, string> = { 'Content-Type': 'application/json' },\n): { url: string; headers: Record<string, string> } {\n  const auth = getEmbedProxyAuth()\n  const headers = { ...baseHeaders }\n  if (auth?.secret) {\n    headers.Authorization = `Bearer ${auth.secret}`\n  }\n  if (auth?.email) {\n    headers['X-Chat-Act-As'] = auth.email\n  }\n  // Optional identity passthrough — only attached when present so the\n  // server's \"required vs optional\" header shape stays exact.\n  if (auth?.firstName) headers['X-Chat-First-Name'] = auth.firstName\n  if (auth?.lastName) headers['X-Chat-Last-Name'] = auth.lastName\n  if (auth?.avatarUrl) headers['X-Chat-Avatar-Url'] = auth.avatarUrl\n  return { url, headers }\n}\n","'use client'\n\n/**\n * Shared `fetch` wrapper for any embedded surface (chat, ticket center,\n * future widgets) that needs to carry the bearer-act-as identity\n * (proxy `Authorization` + `X-Chat-Act-As` headers from\n * `embed-proxy-auth-storage.ts`).\n *\n * Wire header names are `X-Chat-*` for historical reasons — that's a\n * server contract, not a UI namespace. The wrapper itself is generic.\n *\n * Drop-in replacement for `fetch()` — `Authorization` / `X-Chat-Act-As`\n * are merged into `init.headers` when proxy creds are stashed in\n * sessionStorage, otherwise the call falls through to the cookie-auth\n * path unchanged.\n *\n * Use this for any client-side fetch hitting `/api/chat/*`, `/api/docs/chat/*`,\n * or `/api/storage/generate-upload-url` (chat-attachment surface — shared\n * with the ticket center). Routes that do NOT need bearer-act-as\n * (e.g. `/api/profile/me`) keep using vanilla `fetch`.\n */\n\nimport { applyProxyAuth } from './embed-proxy-auth-storage'\n\n// =============================================================================\n// Host-supplied auth adapter (opt-in)\n// =============================================================================\n\n/**\n * Hosts that have their own auth model (cookie sessions, app-specific\n * JWT in localStorage, OAuth access tokens, …) can register an adapter\n * to override the lib's default `embedProxyAuth` flow. When set, the\n * adapter's `getHeaders()` result is merged onto every `embedAuthedFetch`\n * call AFTER the default proxy-auth header step (so adapter headers\n * win over both caller and proxy values), and `credentials` overrides\n * the default `'same-origin'` behaviour.\n *\n * Default (no adapter): MPH-style proxy-impersonation — bearer + act-as\n * read from localStorage, `credentials: 'same-origin'`. No consumer\n * needs to touch this unless they want a different auth model.\n *\n * Use cases:\n *   - openframe-frontend has its own JWT in `localStorage.of_access_token`\n *     and cookie-based session; register an adapter to attach the JWT\n *     and request `credentials: 'include'` so cookies travel cross-origin\n *     to the openframe gateway.\n *   - Future embed hosts with OAuth access tokens, signed URLs, etc.\n *\n * Lifetime: setter is module-level (intentionally — `embedAuthedFetch`\n * is a plain utility, not a hook, so it can't read React context). Host\n * runtime providers should call `setEmbedAuthAdapter(...)` on mount and\n * `setEmbedAuthAdapter(null)` on unmount. Multiple hosts registering at\n * once is a programming error (one chat panel per app).\n */\nexport interface EmbedAuthAdapter {\n  /** Headers merged onto every embedded-fetch call. Return `{}` to add\n   *  nothing. Called per-request so reactive token refresh sees the latest\n   *  value from your auth store / storage. Values typed as\n   *  `string | undefined` so the common narrowed shape\n   *  `{ Authorization: token ? 'Bearer …' : undefined }` (or a conditional\n   *  `token ? { Authorization: … } : {}`) assigns cleanly — `undefined`\n   *  values are filtered before being merged into the request headers. */\n  getHeaders?: () => Record<string, string | undefined>\n  /** `RequestInit.credentials` mode. Default when no adapter: callers'\n   *  `init.credentials` or `'same-origin'`. Use `'include'` for cookie\n   *  auth against a different origin (CORS + `SameSite=None` required). */\n  credentials?: RequestCredentials\n  /**\n   * Optional 401 self-heal. When a request comes back `401`,\n   * `embedAuthedFetch` calls this once, and — if it resolves `true` —\n   * retries the SAME request exactly once with freshly-recomputed\n   * headers (so a rotated bearer from `getHeaders()` is picked up).\n   * Resolve `false` to surface the 401 to the caller unchanged.\n   *\n   * This is the capability the openframe `apiClient` has had all along\n   * (refresh-the-access-token-then-retry); registering it here gives the\n   * embedded chat/ticket surfaces the same self-healing auth instead of\n   * dying on an expired token. Concurrent 401s are de-duplicated by the\n   * wrapper, so this fires at most once per refresh cycle even when a\n   * stampede of chat requests all expire together — your implementation\n   * does NOT need its own in-flight guard (though a token-refresh manager\n   * that already dedups is harmless).\n   *\n   * Keep it idempotent and side-effect-light: on failure the wrapper just\n   * returns the original 401 — logout/redirect decisions belong to the\n   * host's own auth layer, not to this fetch wrapper.\n   */\n  refresh?: () => Promise<boolean>\n  /**\n   * Exact extra origins (`scheme://host[:port]`, as produced by `URL.origin`)\n   * the same-origin guard additionally accepts. The guard normally rejects\n   * every cross-origin URL in production — but native-shell hosts serve the\n   * page from a local pseudo-origin (`capacitor://localhost`,\n   * `tauri://localhost`) with NO server behind it, so the gateway their\n   * bearer already belongs to is unavoidably cross-origin. Listing it here\n   * is the host's explicit sanction to send the adapter's credentials there.\n   * Compared AFTER the http(s)-protocol check, so non-http(s) schemes can\n   * never be allowlisted. Omit (or leave empty) everywhere else — same-origin\n   * remains the rule, this is the narrow exception.\n   */\n  allowedOrigins?: string[]\n}\n\n/**\n * The registered adapter is parked on `globalThis`, NOT in a module-private\n * `let`. Reason: this lib ships multiple entry points (`/utils`,\n * `/components/chat`, …) and a consumer's bundler can inline this file into\n * more than one chunk — giving each chunk its OWN module scope. If the host\n * calls `setEmbedAuthAdapter` from the `/utils` copy while the chat's\n * `embedAuthedFetch` runs from the `/components/chat` copy, a module-local\n * `let` would be set on one copy and read as `null` on the other (the exact\n * \"credentials: same-origin, no Bearer, no refresh\" symptom). A single\n * `globalThis` slot is shared across every copy, so registration always\n * reaches the fetch path.\n */\nconst ADAPTER_GLOBAL_KEY = '__embedAuthedFetchAdapter__'\n\nfunction getRegisteredAuthAdapter(): EmbedAuthAdapter | null {\n  if (typeof globalThis === 'undefined') return null\n  return (globalThis as Record<string, unknown>)[ADAPTER_GLOBAL_KEY] as EmbedAuthAdapter | null ?? null\n}\n\nfunction storeRegisteredAuthAdapter(adapter: EmbedAuthAdapter | null): void {\n  if (typeof globalThis === 'undefined') return\n  ;(globalThis as Record<string, unknown>)[ADAPTER_GLOBAL_KEY] = adapter\n}\n\n/**\n * Register a host-owned auth adapter for `embedAuthedFetch`. Pass `null`\n * to clear (typically on provider unmount).\n *\n * Module-level state — there is one chat panel per app, so a single\n * registration is sufficient. Calling this twice with different non-null\n * adapters replaces the previous one (the most recent registration wins);\n * a `console.warn` flags the overwrite so duplicate-provider mounts get\n * caught in dev.\n */\nexport function setEmbedAuthAdapter(adapter: EmbedAuthAdapter | null): void {\n  if (adapter && getRegisteredAuthAdapter() && process.env.NODE_ENV !== 'production') {\n    console.warn(\n      '[setEmbedAuthAdapter] overwriting a previously-registered auth ' +\n        'adapter. Two chat-runtime providers should not coexist — verify ' +\n        'mount order and pass `null` from the unmounting provider.',\n    )\n  }\n  storeRegisteredAuthAdapter(adapter)\n}\n\n/**\n * Whether a host auth adapter is currently registered. Lets sibling helpers\n * (e.g. `contentFetch`) route through `embedAuthedFetch` ONLY when a host has\n * opted into embedded auth, and stay a plain `fetch` otherwise — so there's a\n * single auth knob (the adapter), not a second content-fetch registration.\n */\nexport function hasEmbedAuthAdapter(): boolean {\n  return getRegisteredAuthAdapter() !== null\n}\n\n/**\n * Whether `url` is an asset the browser CANNOT load natively because its\n * auth rides in request headers.\n *\n * Native asset loads (`<img src>`, `<track src>`, CSS `background-image`)\n * can't carry custom headers, so a URL this returns `true` for must go\n * through `embedAuthedFetch` → blob object-URL instead (see\n * `useAuthedAssetSrc` / `useAuthedImageSrc`).\n *\n * The single signal is the registered adapter: when it supplies an\n * `Authorization` header, the host authenticates by HEADER, so nothing the\n * browser fetches on its own is authenticated. That holds for BOTH shapes a\n * header-auth host takes:\n *\n *   - a cross-origin gateway URL — sanctioned via `allowedOrigins` (native\n *     shells, whose page origin is `capacitor://` / `tauri://`);\n *   - a same-origin / relative reverse-proxy path (`/content/api/...`) —\n *     dev-ticket web, where the token lives in localStorage and there is NO\n *     session cookie to fall back on. This case used to be excluded on the\n *     assumption that same-origin implies \"cookies work\"; true for a cookie\n *     session, false for every header-auth host, which is exactly when this\n *     predicate is consulted at all.\n *\n * Everything else loads natively, unchanged: cookie-auth web (the adapter\n * supplies no `Authorization`), no adapter at all (the hub), and third-party\n * origins the bearer does NOT belong to (public images) — those stay out of\n * reach of the token by the same origin rules `embedAuthedFetch` enforces.\n */\nexport function needsBearerAssetFetch(url: string): boolean {\n  if (typeof window === 'undefined') return false\n  const adapter = getRegisteredAuthAdapter()\n  if (!adapter || adapter.getHeaders?.().Authorization === undefined) return false\n  let target: URL\n  let pageOrigin: string\n  try {\n    target = new URL(url, window.location.href)\n    // Same reason `assertSameOrigin` derives it this way: test environments\n    // mock `window.location` as a plain object with no `origin` field.\n    pageOrigin = new URL(window.location.href).origin\n  } catch {\n    return false\n  }\n  if (target.protocol !== 'http:' && target.protocol !== 'https:') return false\n  if (target.origin === pageOrigin) return true\n  return adapter.allowedOrigins?.includes(target.origin) ?? false\n}\n\n/**\n * `fetch` wrapper that attaches embed-proxy bearer headers (when\n * present in sessionStorage) and forces `credentials: 'same-origin'`\n * so Supabase auth cookies travel too.\n *\n * **Header merge direction (proxy WINS over caller):** the implementation\n * spreads `baseHeaders` first inside `applyProxyAuth`, then sets the\n * `Authorization` / `X-Chat-*` keys — so the proxy values take precedence\n * over anything the caller passed. The motivation is that the bearer +\n * act-as identity is the source of truth for embedded auth; a caller\n * accidentally passing a stale `Authorization` header should NOT override\n * the live proxy creds.\n *\n * **Cross-origin defense:** the wrapper assumes a same-origin `/api/…`\n * relative URL. Absolute URLs are accepted only when their origin matches\n * the current window's origin — or appears in the registered adapter's\n * `allowedOrigins` (the native-shell hatch; see that field's doc) — and\n * cross-origin URLs otherwise throw before the bearer leaves the page.\n * This is a defense-in-depth guard for future call sites — outside the\n * allowlisted-shell case there is no legitimate cross-origin use of this\n * fetch wrapper.\n *\n * **401 self-heal:** when a registered adapter supplies `refresh`, a `401`\n * response triggers a single token refresh + retry of the same request\n * (see `EmbedAuthAdapter.refresh`). This is the openframe `apiClient`'s\n * refresh-then-retry behaviour, lifted into the lib so embedded surfaces\n * no longer need a host-side `window.fetch` monkey-patch to survive an\n * expired access token mid-chat. With no adapter (or no `refresh`), the\n * 401 passes straight through unchanged.\n */\nexport function embedAuthedFetch(url: string, init: RequestInit = {}): Promise<Response> {\n  // Same-origin guard runs SYNCHRONOUSLY (not awaited inside the async\n  // helper below) so a bearer-leaking cross-origin URL throws before any\n  // promise is created — callers and tests rely on the synchronous throw.\n  assertSameOrigin(url)\n\n  // `applyProxyAuth` accepts `Record<string, string>`; normalize the\n  // caller's headers to that shape ONCE, up front. RequestInit accepts\n  // `HeadersInit` which is broader (Headers instance OR array of tuples).\n  // We re-derive the per-request headers from this base on every attempt\n  // (initial + post-refresh retry) so a rotated bearer is picked up.\n  //\n  // When the caller passes no headers, fall back to the same default\n  // `applyProxyAuth` uses internally — `Content-Type: application/json` —\n  // so JSON POSTs keep their content-type when only `embedAuthedFetch(url)`\n  // is used at the call site. GET callers that explicitly want no body\n  // headers can pass `init.headers = {}` to opt out.\n  let baseHeaders: Record<string, string>\n  if (init.headers === undefined) {\n    baseHeaders = { 'Content-Type': 'application/json' }\n  } else {\n    baseHeaders = {}\n    if (init.headers instanceof Headers) {\n      init.headers.forEach((v, k) => {\n        baseHeaders[k] = v\n      })\n    } else if (Array.isArray(init.headers)) {\n      for (const [k, v] of init.headers) baseHeaders[k] = v\n    } else {\n      Object.assign(baseHeaders, init.headers as Record<string, string>)\n    }\n  }\n\n  return fetchWithRefresh(url, init, baseHeaders, false)\n}\n\n/**\n * Single in-flight refresh shared across all concurrent `embedAuthedFetch`\n * callers. A stampede of chat requests that all 401 at the same moment must\n * trigger the adapter's `refresh()` ONCE, not N times — otherwise an\n * expiring session fires a thundering herd of refresh calls at the auth\n * server. Resets to `null` once settled so the next genuine expiry can\n * refresh again.\n */\n// Stored on `globalThis` rather than a module-local so the \"single refresh\"\n// guarantee survives module duplication. Bundlers can ship more than one copy\n// of this module (e.g. across chunks or a host + embedded build); a per-module\n// variable would let each copy run its own refresh cycle, re-creating the\n// thundering-herd this dedupe exists to prevent.\nconst IN_FLIGHT_REFRESH_GLOBAL_KEY = '__embedAuthedFetchInFlightRefresh__'\n\nfunction getInFlightRefresh(): Promise<boolean> | null {\n  if (typeof globalThis === 'undefined') return null\n  return (\n    ((globalThis as Record<string, unknown>)[IN_FLIGHT_REFRESH_GLOBAL_KEY] as\n      | Promise<boolean>\n      | null\n      | undefined) ?? null\n  )\n}\n\nfunction setInFlightRefresh(refresh: Promise<boolean> | null): void {\n  if (typeof globalThis === 'undefined') return\n  ;(globalThis as Record<string, unknown>)[IN_FLIGHT_REFRESH_GLOBAL_KEY] = refresh\n}\n\nfunction dedupedRefresh(): Promise<boolean> {\n  const adapter = getRegisteredAuthAdapter()\n  if (!adapter?.refresh) return Promise.resolve(false)\n  let inFlightRefresh = getInFlightRefresh()\n  if (!inFlightRefresh) {\n    // Wrap in `Promise.resolve` so an adapter that throws synchronously\n    // (rather than rejecting) still funnels through the shared slot and\n    // clears it. A rejected refresh is treated as \"could not refresh\".\n    inFlightRefresh = Promise.resolve()\n      .then(() => adapter.refresh!())\n      .catch(() => false)\n      .finally(() => {\n        setInFlightRefresh(null)\n      })\n    setInFlightRefresh(inFlightRefresh)\n  }\n  return inFlightRefresh\n}\n\n/**\n * Core fetch path: merge proxy-auth + adapter headers, issue the request,\n * and — on a `401` with a refresh-capable adapter — refresh once and retry\n * the identical request a single time. Mirrors the openframe `apiClient`'s\n * refresh-then-retry contract (`isRetry` guards against infinite loops).\n */\nasync function fetchWithRefresh(\n  url: string,\n  init: RequestInit,\n  baseHeaders: Record<string, string>,\n  isRetry: boolean,\n): Promise<Response> {\n  // Re-run the merge each attempt: `applyProxyAuth` reads the latest stored\n  // proxy creds and `getHeaders()` reads the latest bearer, so a retry after\n  // refresh carries the rotated token rather than the stale one. `{...baseHeaders}`\n  // keeps the caller's normalized headers immutable across attempts.\n  const { url: authedUrl, headers } = applyProxyAuth(url, { ...baseHeaders })\n\n  // Host-supplied auth adapter layer. Runs AFTER the proxy-auth merge so\n  // adapter headers override both caller and proxy values — the adapter\n  // is the host's explicit \"this is my auth model\" override, intentionally\n  // last-writer-wins. When no adapter is registered, this is a zero-cost\n  // no-op (object spread of `{}`).\n  const adapter = getRegisteredAuthAdapter()\n  if (adapter?.getHeaders) {\n    // Filter `undefined` values — the adapter type allows them so consumers\n    // don't have to narrow `{ Authorization: token ? '…' : undefined }`-shaped\n    // returns, but `fetch` headers must be strings.\n    for (const [k, v] of Object.entries(adapter.getHeaders())) {\n      if (v !== undefined) headers[k] = v\n    }\n  }\n  const credentials = adapter?.credentials ?? init.credentials ?? 'same-origin'\n\n  const response = await fetch(authedUrl, {\n    ...init,\n    headers,\n    // Default `same-origin` carries Supabase cookies for the MPH proxy-\n    // auth model. Hosts on different origins (openframe-frontend ↔\n    // openframe gateway) register `credentials: 'include'` via the\n    // adapter to make their own cookies travel cross-origin (CORS +\n    // `SameSite=None` must be configured server-side for that to work).\n    credentials,\n  })\n\n  // 401 self-heal: refresh the token once and retry. Only when an adapter\n  // opted into `refresh`, and only on the first attempt — a 401 on the\n  // retry means the fresh token is also unauthorized, so surface it.\n  if (response.status === 401 && !isRetry && adapter?.refresh) {\n    const refreshed = await dedupedRefresh()\n    if (refreshed) {\n      return fetchWithRefresh(url, init, baseHeaders, true)\n    }\n  }\n\n  return response\n}\n\n/**\n * Reject any URL that resolves to a cross-origin destination or to a\n * non-http(s) scheme. Every input is resolved against\n * `window.location.href` so the same rule covers path-only\n * (`/api/...`), absolute (`https://...`), protocol-relative\n * (`//host/...`), AND whitespace-prefixed forms (`\\t//evil.com/...`) —\n * the WHATWG fetch spec strips leading ASCII whitespace before\n * parsing, so any regex-based \"skip relative\" shortcut is bypassable\n * with a leading `\\t`/`\\n`/`\\r`/space. We resolve unconditionally\n * instead and compare origins.\n *\n * Also blocks `javascript:` / `data:` / `blob:` etc. — only `http(s):`\n * is allowed. This is explicit allowlisting rather than relying on\n * `origin === 'null'` to fall out wrong.\n *\n * Server-side rendering: when `typeof window === 'undefined'` we skip\n * the check — the bearer comes from sessionStorage which doesn't exist\n * on the server, so there's nothing to leak anyway.\n */\nfunction assertSameOrigin(url: string): void {\n  if (typeof window === 'undefined') return\n  let target: URL\n  let pageOrigin: string\n  try {\n    target = new URL(url, window.location.href)\n    // Derive the page origin from `href` rather than reading\n    // `window.location.origin` directly so the check works in test\n    // environments that mock `window.location` to a plain object\n    // without an `origin` field (jsdom setups do this).\n    pageOrigin = new URL(window.location.href).origin\n  } catch {\n    throw new Error(`embedAuthedFetch: refusing to fetch malformed URL (${JSON.stringify(url)})`)\n  }\n  if (target.protocol !== 'http:' && target.protocol !== 'https:') {\n    throw new Error(\n      `embedAuthedFetch: refusing non-http(s) URL (${target.protocol}) — pass a relative /api/* path instead`,\n    )\n  }\n  if (target.origin !== pageOrigin) {\n    // Host-sanctioned cross-origin target (`EmbedAuthAdapter.allowedOrigins`).\n    // Native shells serve the page from a local pseudo-origin with no server\n    // behind it, so their gateway — the same backend the host already trusts\n    // with this bearer — is unavoidably cross-origin. Exact `URL.origin`\n    // match, and only reachable for http(s) targets (checked above).\n    if (getRegisteredAuthAdapter()?.allowedOrigins?.includes(target.origin)) {\n      return\n    }\n    // Dev-mode escape hatch — embedded apps (e.g. openframe-frontend)\n    // run on a different origin from their gateway during local dev,\n    // and forcing a Next.js `rewrites()` workaround is more error-prone\n    // than relaxing the guard for the dev build. In production\n    // (`NODE_ENV === 'production'`) the guard stays absolute — same\n    // defense-in-depth bearer-leak protection as before. The check is\n    // baked at build time by Next/webpack/Turbopack so prod bundles\n    // contain only the throwing branch (no dev string in the artifact).\n    if (process.env.NODE_ENV !== 'production') {\n      console.warn(\n        `[embedAuthedFetch] cross-origin fetch to ${target.origin} ` +\n          `allowed in dev (NODE_ENV !== 'production'). Production builds ` +\n          `will reject this — wire a same-origin proxy before shipping.`,\n      )\n      return\n    }\n    throw new Error(\n      `embedAuthedFetch: refusing cross-origin fetch to ${target.origin} — pass a relative /api/* path instead`,\n    )\n  }\n}\n","'use client'\n\n/**\n * `contentFetch` — the fetch the lib's SELF-FETCHING content surfaces use:\n * roadmap (list + vote + per-task refresh), delivery, product releases,\n * onboarding guides (catalog + detail), legal docs, and the release-detail\n * injected roadmap/delivery sections.\n *\n * It deliberately reuses the SINGLE embed-auth knob — the registered\n * `EmbedAuthAdapter` — instead of introducing a second registration:\n *   - adapter registered (host opted into embedded auth, e.g. openframe-frontend\n *     for its embedded chat) → route through `embedAuthedFetch`, so content GETs/\n *     POSTs carry the SAME bearer/cookie + 401-refresh as the chat.\n *   - no adapter (the public hub, or the embedding example whose proxy injects\n *     auth server-side) → a plain, byte-for-byte unchanged `fetch`.\n *\n * So a host wires auth ONCE (the chat adapter) and content inherits it; there is\n * no content-specific fetcher to configure.\n */\n\nimport { embedAuthedFetch, hasEmbedAuthAdapter } from './embed-authed-fetch'\n\nexport const contentFetch: typeof fetch = (input, init) => {\n  if (!hasEmbedAuthAdapter()) return fetch(input, init)\n  // embedAuthedFetch takes a string url; coerce the broader `fetch` input shape\n  // (content surfaces always pass strings, but handle URL/Request defensively).\n  const url =\n    typeof input === 'string' ? input : input instanceof URL ? input.href : (input as Request).url\n  return embedAuthedFetch(url, init)\n}\n","'use client';\n\nimport { useCallback, useState } from 'react';\nimport { useToast } from './use-toast';\n\ninterface UseCopyToClipboardOptions {\n  successTitle?: string;\n  successDescription?: string;\n  errorTitle?: string;\n  errorDescription?: string;\n  resetDelay?: number;\n}\n\nexport function useCopyToClipboard({\n  successTitle = 'Copied',\n  successDescription = 'Copied to clipboard',\n  errorTitle = 'Copy failed',\n  errorDescription = 'Could not copy to clipboard',\n  resetDelay = 2000,\n}: UseCopyToClipboardOptions = {}) {\n  const { toast } = useToast();\n  const [copied, setCopied] = useState(false);\n\n  const copy = useCallback(\n    async (text: string) => {\n      try {\n        await navigator.clipboard.writeText(text);\n        setCopied(true);\n        toast({ title: successTitle, description: successDescription, variant: 'success' });\n        setTimeout(() => setCopied(false), resetDelay);\n      } catch {\n        toast({ title: errorTitle, description: errorDescription, variant: 'destructive' });\n      }\n    },\n    [successTitle, successDescription, errorTitle, errorDescription, resetDelay, toast],\n  );\n\n  return { copy, copied };\n}\n","import { useEffect, useMemo, useState, useRef } from 'react'\n\n/**\n * Configuration for batch image fetching\n */\nexport interface BatchImageFetchConfig {\n  /** Base URL for tenant-specific API calls (e.g., 'https://tenant.openframe.dev' or '') */\n  tenantHostUrl?: string\n  /** Enable dev mode with Bearer token from localStorage */\n  enableDevMode?: boolean\n  /** localStorage key for access token (default: 'of_access_token') */\n  accessTokenKey?: string\n}\n\n/**\n * Global configuration for batch image fetching\n * Can be set once at app initialization\n */\nlet globalBatchImageConfig: BatchImageFetchConfig = {}\n\n/**\n * Configure global settings for batch image fetching\n * Call this once in your app initialization (e.g., _app.tsx or layout.tsx)\n *\n * @example\n * ```typescript\n * // In app initialization\n * configureBatchImageFetch({\n *   tenantHostUrl: process.env.NEXT_PUBLIC_TENANT_HOST_URL || '',\n *   enableDevMode: process.env.NEXT_PUBLIC_ENABLE_DEV_TICKET_OBSERVER === 'true'\n * })\n * ```\n */\nexport function configureBatchImageFetch(config: BatchImageFetchConfig): void {\n  globalBatchImageConfig = { ...globalBatchImageConfig, ...config }\n}\n\n/**\n * Get current batch image fetch configuration\n */\nfunction getBatchImageConfig(): Required<BatchImageFetchConfig> {\n  return {\n    tenantHostUrl: globalBatchImageConfig.tenantHostUrl || '',\n    enableDevMode: globalBatchImageConfig.enableDevMode ?? false,\n    accessTokenKey: globalBatchImageConfig.accessTokenKey || 'of_access_token'\n  }\n}\n\n/**\n * Fetch multiple images with authentication in batch\n * Returns a map of original imageUrl to fetched blob URL\n *\n * @param imageUrls - Array of image URLs to fetch\n * @param config - Optional configuration override for this batch\n * @returns Promise resolving to map of original URL → blob URL\n *\n * @example\n * ```typescript\n * const images = await batchFetchAuthenticatedImages([\n *   '/api/organizations/123/image',\n *   '/api/organizations/456/image'\n * ])\n * // { '/api/organizations/123/image': 'blob:...', '/api/organizations/456/image': 'blob:...' }\n * ```\n */\nexport async function batchFetchAuthenticatedImages(\n  imageUrls: string[],\n  config?: BatchImageFetchConfig\n): Promise<Record<string, string | undefined>> {\n  const results: Record<string, string | undefined> = {}\n\n  if (imageUrls.length === 0) {\n    return results\n  }\n\n  const { tenantHostUrl, enableDevMode, accessTokenKey } = {\n    ...getBatchImageConfig(),\n    ...config\n  }\n\n  const fetchPromises = imageUrls.map(async (imageUrl) => {\n    try {\n      // Construct full image URL\n      let fullImageUrl: string\n      if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {\n        fullImageUrl = imageUrl\n      } else if (imageUrl.startsWith('/api/')) {\n        fullImageUrl = `${tenantHostUrl}${imageUrl}`\n      } else if (imageUrl.startsWith('/')) {\n        fullImageUrl = `${tenantHostUrl}/api${imageUrl}`\n      } else {\n        fullImageUrl = `${tenantHostUrl}/api/${imageUrl}`\n      }\n\n      // Add cache buster\n      const cacheBuster = `?t=${Date.now()}`\n      fullImageUrl = fullImageUrl + cacheBuster\n\n      // Prepare headers\n      const headers: Record<string, string> = {\n        'Accept': 'image/*',\n        'Cache-Control': 'no-cache, no-store, must-revalidate',\n        'Pragma': 'no-cache'\n      }\n\n      // Add Bearer token in dev mode\n      if (enableDevMode) {\n        try {\n          const accessToken = localStorage.getItem(accessTokenKey)\n          if (accessToken) {\n            headers['Authorization'] = `Bearer ${accessToken}`\n          }\n        } catch (error) {\n          // Silently continue without token\n        }\n      }\n\n      // Fetch image\n      const response = await fetch(fullImageUrl, {\n        method: 'GET',\n        credentials: 'include', // Include cookies for authentication\n        headers\n      })\n\n      if (!response.ok) {\n        throw new Error(`Failed to fetch image: ${response.status}`)\n      }\n\n      // Convert to blob URL\n      const blob = await response.blob()\n      const objectUrl = URL.createObjectURL(blob)\n\n      return { imageUrl, fetchedUrl: objectUrl }\n    } catch (error) {\n      console.warn(`Failed to fetch image ${imageUrl}:`, error)\n      return { imageUrl, fetchedUrl: undefined }\n    }\n  })\n\n  const fetchResults = await Promise.all(fetchPromises)\n\n  fetchResults.forEach(({ imageUrl, fetchedUrl }) => {\n    results[imageUrl] = fetchedUrl\n  })\n\n  return results\n}\n\n/**\n * React hook to batch fetch images with authentication\n *\n * Features:\n * - Automatically deduplicates URLs\n * - Caches fetched results\n * - Only fetches new/unfetched URLs\n * - Cleans up blob URLs on unmount\n *\n * @param imageUrls - Array of image URLs (can include null/undefined)\n * @param config - Optional configuration override\n * @returns Map of original URL → fetched blob URL\n *\n * @example\n * ```typescript\n * // In a component\n * const imageUrls = useMemo(() =>\n *   organizations.map(org => org.imageUrl).filter(Boolean),\n *   [organizations]\n * )\n * const fetchedImages = useBatchImages(imageUrls)\n *\n * // Use in render\n * <img src={fetchedImages[org.imageUrl]} alt={org.name} />\n * ```\n */\nexport function useBatchImages(\n  imageUrls: (string | null | undefined)[],\n  config?: BatchImageFetchConfig\n): Record<string, string | undefined> {\n  const [fetchedImages, setFetchedImages] = useState<Record<string, string | undefined>>({})\n  const [loading, setLoading] = useState(false)\n\n  // Deduplicate and filter out null/undefined\n  const uniqueUrls = useMemo(() =>\n    Array.from(new Set(imageUrls.filter((url): url is string => Boolean(url)))),\n    [imageUrls]\n  )\n\n  // Track URLs we've already requested to avoid duplicate fetches\n  const requestedUrls = useRef<Set<string>>(new Set())\n\n  useEffect(() => {\n    if (uniqueUrls.length === 0) {\n      setFetchedImages({})\n      return\n    }\n\n    // Find URLs we haven't fetched yet\n    const urlsToFetch = uniqueUrls.filter(url => !requestedUrls.current.has(url))\n\n    if (urlsToFetch.length === 0) {\n      return // All URLs already requested\n    }\n\n    // Mark these URLs as requested\n    urlsToFetch.forEach(url => requestedUrls.current.add(url))\n\n    setLoading(true)\n\n    batchFetchAuthenticatedImages(urlsToFetch, config)\n      .then(newResults => {\n        setFetchedImages(prev => ({ ...prev, ...newResults }))\n      })\n      .catch(error => {\n        console.error('Failed to batch fetch images:', error)\n      })\n      .finally(() => {\n        setLoading(false)\n      })\n\n    // Cleanup blob URLs on unmount\n    return () => {\n      Object.values(fetchedImages).forEach(blobUrl => {\n        if (blobUrl && blobUrl.startsWith('blob:')) {\n          URL.revokeObjectURL(blobUrl)\n        }\n      })\n    }\n  }, [uniqueUrls, config])\n\n  return fetchedImages\n}\n","import { useEffect, useState, useRef } from 'react'\n\n/**\n * Configuration for single image fetching\n * Uses same config as batch image fetching for consistency\n */\nexport interface AuthenticatedImageConfig {\n  /** Base URL for tenant-specific API calls (e.g., 'https://tenant.openframe.dev' or '') */\n  tenantHostUrl?: string\n  /** Enable dev mode with Bearer token from localStorage */\n  enableDevMode?: boolean\n  /** localStorage key for access token (default: 'of_access_token') */\n  accessTokenKey?: string\n}\n\n/**\n * Global configuration for authenticated image fetching\n * Shared with useBatchImages for consistency\n */\nlet globalImageConfig: AuthenticatedImageConfig = {}\n\n/**\n * Global cache for authenticated images\n * Stores blob URLs by cache key\n */\ninterface ImageCacheEntry {\n  blobUrl: string\n  timestamp: number\n  refCount: number\n}\n\nconst imageCache = new Map<string, ImageCacheEntry>()\nconst pendingRequests = new Map<string, Promise<string | undefined>>()\n\n/**\n * Cache cleanup interval (5 minutes)\n */\nconst CACHE_CLEANUP_INTERVAL = 5 * 60 * 1000\n\n/**\n * Cache entry max age (30 minutes)\n */\nconst CACHE_MAX_AGE = 30 * 60 * 1000\n\n/**\n * Clean up expired cache entries\n */\nfunction cleanupImageCache() {\n  const now = Date.now()\n  for (const [key, entry] of imageCache.entries()) {\n    if (entry.refCount === 0 && now - entry.timestamp > CACHE_MAX_AGE) {\n      URL.revokeObjectURL(entry.blobUrl)\n      imageCache.delete(key)\n    }\n  }\n}\n\n/**\n * Periodic cache cleanup\n */\nif (typeof window !== 'undefined') {\n  setInterval(cleanupImageCache, CACHE_CLEANUP_INTERVAL)\n}\n\n/**\n * Configure global settings for authenticated image fetching\n * Call this once in your app initialization (e.g., _app.tsx or layout.tsx)\n *\n * Note: This uses the same configuration as useBatchImages. If you've already\n * called configureBatchImageFetch(), you don't need to call this separately.\n *\n * @example\n * ```typescript\n * // In app initialization\n * configureAuthenticatedImage({\n *   tenantHostUrl: process.env.NEXT_PUBLIC_TENANT_HOST_URL || '',\n *   enableDevMode: process.env.NEXT_PUBLIC_ENABLE_DEV_TICKET_OBSERVER === 'true'\n * })\n * ```\n */\nexport function configureAuthenticatedImage(config: AuthenticatedImageConfig): void {\n  globalImageConfig = { ...globalImageConfig, ...config }\n}\n\n/**\n * Get current authenticated image configuration\n */\nfunction getImageConfig(): Required<AuthenticatedImageConfig> {\n  return {\n    tenantHostUrl: globalImageConfig.tenantHostUrl || '',\n    enableDevMode: globalImageConfig.enableDevMode ?? false,\n    accessTokenKey: globalImageConfig.accessTokenKey || 'of_access_token'\n  }\n}\n\n/**\n * React hook to fetch a single image with authentication\n *\n * Features:\n * - Fetches image with cookie authentication\n * - Optional Bearer token in dev mode\n * - Converts to blob URL for img src\n * - Automatic cleanup of blob URLs\n * - Cache-busting with refreshKey\n * - Loading and error states\n * - **Global caching** - Prevents duplicate requests for identical URLs\n * - **Automatic deduplication** - Multiple components using same URL share cached result\n * - **Reference counting** - Cached blobs cleaned up when no longer used\n *\n * @param imageUrl - The image URL to fetch (null/undefined = no fetch)\n * @param refreshKey - Optional key to force re-fetch (e.g., version number, timestamp)\n * @param config - Optional configuration override\n * @returns Object with imageUrl (blob), isLoading, and error\n *\n * @example\n * ```typescript\n * // Basic usage\n * const { imageUrl, isLoading, error } = useAuthenticatedImage(\n *   organization?.imageUrl\n * )\n *\n * // With refresh key (e.g., after upload)\n * const { imageUrl } = useAuthenticatedImage(\n *   organization?.imageUrl,\n *   organization?.imageVersion // Timestamp or version number\n * )\n *\n * // In render\n * {imageUrl && <img src={imageUrl} alt=\"Organization\" />}\n * ```\n */\nexport function useAuthenticatedImage(\n  imageUrl?: string | null,\n  refreshKey?: string | number,\n  config?: AuthenticatedImageConfig\n): {\n  imageUrl: string | undefined\n  isLoading: boolean\n  error: string | null\n} {\n  const [fetchedImageUrl, setFetchedImageUrl] = useState<string | undefined>()\n  const [isLoading, setIsLoading] = useState(false)\n  const [error, setError] = useState<string | null>(null)\n  const currentCacheKeyRef = useRef<string | null>(null)\n\n  useEffect(() => {\n    if (!imageUrl) {\n      setFetchedImageUrl(undefined)\n      setIsLoading(false)\n      setError(null)\n      \n      if (currentCacheKeyRef.current) {\n        const entry = imageCache.get(currentCacheKeyRef.current)\n        if (entry) {\n          entry.refCount--\n        }\n        currentCacheKeyRef.current = null\n      }\n      return\n    }\n\n    const { tenantHostUrl, enableDevMode, accessTokenKey } = {\n      ...getImageConfig(),\n      ...config\n    }\n\n    // Construct full image URL\n    let fullImageUrl: string\n    if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {\n      fullImageUrl = imageUrl\n    } else if (imageUrl.startsWith('/api/')) {\n      fullImageUrl = `${tenantHostUrl}${imageUrl}`\n    } else if (imageUrl.startsWith('/')) {\n      fullImageUrl = `${tenantHostUrl}/api${imageUrl}`\n    } else {\n      fullImageUrl = `${tenantHostUrl}/api/${imageUrl}`\n    }\n\n    // Create cache key (use refreshKey if provided, otherwise no cache buster for caching)\n    const cacheKey = refreshKey ? `${fullImageUrl}?v=${refreshKey}` : fullImageUrl\n    \n    if (currentCacheKeyRef.current && currentCacheKeyRef.current !== cacheKey) {\n      const prevEntry = imageCache.get(currentCacheKeyRef.current)\n      if (prevEntry) {\n        prevEntry.refCount--\n      }\n    }\n    \n    currentCacheKeyRef.current = cacheKey\n\n    const cachedEntry = imageCache.get(cacheKey)\n    if (cachedEntry) {\n      cachedEntry.refCount++\n      cachedEntry.timestamp = Date.now()\n      \n      setFetchedImageUrl(cachedEntry.blobUrl)\n      setIsLoading(false)\n      setError(null)\n      return\n    }\n\n    const pendingRequest = pendingRequests.get(cacheKey)\n    if (pendingRequest) {\n      setIsLoading(true)\n      setError(null)\n      \n      pendingRequest\n        .then(blobUrl => {\n          if (blobUrl) {\n            const entry = imageCache.get(cacheKey)\n            if (entry) {\n              entry.refCount++\n              setFetchedImageUrl(blobUrl)\n            }\n          }\n          setIsLoading(false)\n        })\n        .catch(err => {\n          setError(err instanceof Error ? err.message : 'Failed to fetch image')\n          setIsLoading(false)\n        })\n      return\n    }\n\n    setIsLoading(true)\n    setError(null)\n\n    const requestUrl = refreshKey ? cacheKey : `${fullImageUrl}?t=${Date.now()}`\n\n    // Prepare headers\n    const headers: Record<string, string> = {\n      'Accept': 'image/*',\n      'Cache-Control': 'no-cache, no-store, must-revalidate',\n      'Pragma': 'no-cache'\n    }\n\n    // Add Bearer token in dev mode\n    if (enableDevMode) {\n      try {\n        const accessToken = localStorage.getItem(accessTokenKey)\n        if (accessToken) {\n          headers['Authorization'] = `Bearer ${accessToken}`\n        }\n      } catch (error) {\n        // Silently continue without token\n      }\n    }\n\n    const fetchPromise = fetch(requestUrl, {\n      method: 'GET',\n      credentials: 'include', // Include cookies for authentication\n      headers\n    })\n      .then(response => {\n        if (!response.ok) {\n          throw new Error(`Failed to fetch image: ${response.status}`)\n        }\n        return response.blob()\n      })\n      .then(blob => {\n        const objectUrl = URL.createObjectURL(blob)\n        \n        imageCache.set(cacheKey, {\n          blobUrl: objectUrl,\n          timestamp: Date.now(),\n          refCount: 1\n        })\n        \n        setFetchedImageUrl(objectUrl)\n        setIsLoading(false)\n        return objectUrl\n      })\n      .catch(err => {\n        setError(err instanceof Error ? err.message : 'Failed to fetch image')\n        setFetchedImageUrl(undefined)\n        setIsLoading(false)\n        throw err\n      })\n      .finally(() => {\n        pendingRequests.delete(cacheKey)\n      })\n\n    pendingRequests.set(cacheKey, fetchPromise)\n\n  }, [imageUrl, refreshKey, config])\n\n  useEffect(() => {\n    return () => {\n      if (currentCacheKeyRef.current) {\n        const entry = imageCache.get(currentCacheKeyRef.current)\n        if (entry) {\n          entry.refCount--\n        }\n      }\n    }\n  }, [])\n\n  return { imageUrl: fetchedImageUrl, isLoading, error }\n}\n","'use client'\n\nimport { useEffect, useReducer } from 'react'\nimport { embedAuthedFetch, needsBearerAssetFetch } from '../utils/embed-authed-fetch'\n\n/**\n * Resolve ANY asset URL the browser would load natively — `<img src>`,\n * `<track src>`, a font, a poster — for hosts whose auth rides in request\n * HEADERS instead of cookies: native shells (`capacitor://`, `tauri://`) and\n * dev-ticket web, both in bearer mode. A native subresource load can't carry\n * an `Authorization` header, so a gateway URL that works on the cookie-auth\n * web 401s there.\n *\n * When `needsBearerAssetFetch` says the URL is bearer-authed, this hook\n * fetches it through `embedAuthedFetch` (bearer + deduped 401-refresh-retry +\n * the adapter's cross-origin guard — the SAME single auth knob every embedded\n * `fetch` in the lib rides) and returns a blob object-URL. Every other URL is\n * returned unchanged, so on the cookie-auth web the hook is a pass-through.\n *\n * Returns `undefined` while the fetch is in flight OR after it fails, so\n * callers show their existing placeholder / \"no captions\" branch instead of a\n * broken asset; a failed fetch retries on the next mount.\n *\n * Cache: module-level, session-lifetime, keyed by full URL — so the two\n * `<track>` URLs of one player, or the same avatar in twenty rows, cost one\n * request. Gateway URLs carry a `?v=<content-hash>` cache-buster, so changed\n * content is a new key; entries are never revoked (a VTT / avatar is a few KB\n * — refcount churn isn't worth it) and are dropped wholesale at session end\n * via `clearAuthedAssetCache`.\n */\n\nconst resolvedCache = new Map<string, string>()\nconst inFlight = new Map<string, Promise<void>>()\nlet cacheGeneration = 0\n\n/**\n * Drop every cached blob and revoke its object URL. Hosts call this at\n * session end (logout / forced re-login) so a follow-on login as a\n * different identity cannot be served blobs fetched under the previous\n * one's bearer. Fetches still in flight when the clear happens are fenced\n * by a generation counter — their results are revoked instead of cached.\n */\nexport function clearAuthedAssetCache(): void {\n  cacheGeneration++\n  for (const url of resolvedCache.values()) URL.revokeObjectURL(url)\n  resolvedCache.clear()\n  inFlight.clear()\n}\n\nfunction fetchAsBlobUrl(src: string, accept: string): Promise<void> {\n  let pending = inFlight.get(src)\n  if (!pending) {\n    const startedGeneration = cacheGeneration\n    pending = embedAuthedFetch(src, { headers: { Accept: accept } })\n      .then(async response => {\n        if (!response.ok) throw new Error(`asset fetch failed: ${response.status}`)\n        const blobUrl = URL.createObjectURL(await response.blob())\n        if (startedGeneration === cacheGeneration) {\n          resolvedCache.set(src, blobUrl)\n        } else {\n          URL.revokeObjectURL(blobUrl)\n        }\n      })\n      .finally(() => {\n        if (inFlight.get(src) === pending) inFlight.delete(src)\n      })\n    inFlight.set(src, pending)\n  }\n  return pending\n}\n\n/**\n * @param src    the asset URL (relative or absolute), or null/undefined\n * @param accept `Accept` header for the authed fetch — narrow it to what the\n *               consumer renders (`image/*`, `text/vtt`); it also replaces\n *               `embedAuthedFetch`'s default JSON `Content-Type` on the GET.\n */\nexport function useAuthedAssetSrc(src?: string | null, accept = '*/*'): string | undefined {\n  const bearerSrc = src && needsBearerAssetFetch(src) ? src : null\n  const [, rerender] = useReducer((c: number) => c + 1, 0)\n\n  useEffect(() => {\n    if (!bearerSrc || resolvedCache.has(bearerSrc)) return\n    let cancelled = false\n    fetchAsBlobUrl(bearerSrc, accept)\n      .catch(() => {})\n      .finally(() => {\n        if (!cancelled) rerender()\n      })\n    return () => {\n      cancelled = true\n    }\n  }, [bearerSrc, accept])\n\n  if (!src) return undefined\n  if (!bearerSrc) return src\n  return resolvedCache.get(bearerSrc)\n}\n","'use client'\n\nimport { clearAuthedAssetCache, useAuthedAssetSrc } from './use-authed-asset-src'\n\n/**\n * Image flavor of `useAuthedAssetSrc` — see that hook for the full contract.\n * Kept as its own export because it is the lib's most-used consumer (avatars,\n * entity images, markdown `<img>`) and because hosts already import this name.\n *\n * Distinct from the legacy `useAuthenticatedImage`, which ALWAYS blob-fetches\n * with `credentials: 'include'` and its own global config. This hook is\n * pass-through-first and keys off the single auth knob the lib already has:\n * the registered `EmbedAuthAdapter`.\n */\nexport function useAuthedImageSrc(src?: string | null): string | undefined {\n  return useAuthedAssetSrc(src, 'image/*')\n}\n\n/**\n * Session-end cache drop. Now an alias of `clearAuthedAssetCache` — one cache\n * backs every authed asset (images AND caption tracks), so hosts keep calling\n * this single function on logout / forced re-login.\n */\nexport const clearAuthedImageCache = clearAuthedAssetCache\n","/**\n * useQueryParams Hook - GraphQL Integration for URL State Management\n *\n * Automatically generates URL state management from GraphQL queries.\n * Parses query AST at runtime, flattens nested input types, and syncs with URL.\n *\n * @example\n * const LOGS_QUERY = gql`\n *   query GetLogs($search: String, $filter: LogFilterInput) { ... }\n * `\n *\n * const { variables, setParam } = useQueryParams(LOGS_QUERY)\n * const { data } = useQuery(LOGS_QUERY, { variables })\n *\n * // URL: /logs?search=error&severity=critical\n * // variables: { search: 'error', filter: { severity: ['critical'] } }\n */\n\n'use client'\n\nimport { useEffect, useState, useMemo, useCallback } from 'react'\nimport { useRouter, useSearchParams } from '../../embed-shims/next-navigation'\nimport { DocumentNode } from 'graphql'\n\nimport { extractVariablesFromQuery } from './graphql-parser'\nimport { flattenQueryVariables, mergeDefaults, validateSchema, FlattenedParam } from './flatten-schema'\nimport { urlParamsToVariables, variablesToUrlParams, mergeVariables, clearParams } from './url-converter'\nimport { introspector } from './introspection'\n\n/**\n * Options for useQueryParams hook\n */\nexport interface UseQueryParamsOptions {\n  /** Default values for parameters */\n  defaultValues?: Record<string, any>\n\n  /** GraphQL endpoint for introspection (defaults to process.env.NEXT_PUBLIC_API_URL/graphql) */\n  introspectionEndpoint?: string\n\n  /** HTTP headers for introspection (e.g., authentication) */\n  introspectionHeaders?: Record<string, string>\n\n  /** Skip introspection (use only AST parsing, no nested type flattening) */\n  skipIntrospection?: boolean\n\n  /** Custom parameter name mapping (override auto-generated names) */\n  paramMapping?: Record<string, string>\n\n  /** Enable debug logging */\n  debug?: boolean\n}\n\n/**\n * Return type for useQueryParams hook\n */\nexport interface UseQueryParamsReturn<TVariables = Record<string, any>> {\n  /** GraphQL variables ready for Apollo Client */\n  variables: TVariables\n\n  /** Raw URL parameters (before conversion to variables) */\n  params: Record<string, any>\n\n  /** Flattened parameter schema */\n  schema: Record<string, FlattenedParam>\n\n  /** Set a single parameter */\n  setParam: (key: string, value: any) => void\n\n  /** Set multiple parameters at once */\n  setParams: (params: Record<string, any>) => void\n\n  /** Clear specific parameters */\n  clearParams: (keys: string[]) => void\n\n  /** Reset all parameters (clear URL) */\n  resetParams: () => void\n\n  /** Whether schema is ready (introspection complete) */\n  isReady: boolean\n\n  /** Loading state during initialization */\n  isLoading: boolean\n\n  /** Error during initialization */\n  error: Error | null\n}\n\n/**\n * useQueryParams - Auto-generate URL state from GraphQL query\n *\n * This hook:\n * 1. Parses GraphQL query AST to extract variable definitions\n * 2. Fetches GraphQL schema via introspection (optional, cached)\n * 3. Flattens nested input types to simple URL parameters\n * 4. Syncs URL ↔ GraphQL variables bidirectionally\n * 5. Provides type-safe parameter updates\n *\n * @param query - GraphQL DocumentNode (from gql`` template tag)\n * @param options - Configuration options\n * @returns Hook API for managing URL state\n */\nexport function useQueryParams<TVariables = Record<string, any>>(\n  query: DocumentNode,\n  options: UseQueryParamsOptions = {}\n): UseQueryParamsReturn<TVariables> {\n  const router = useRouter()\n  const searchParams = useSearchParams()\n\n  const [isLoading, setIsLoading] = useState(true)\n  const [isReady, setIsReady] = useState(false)\n  const [error, setError] = useState<Error | null>(null)\n  const [schema, setSchema] = useState<Record<string, FlattenedParam>>({})\n\n  // Extract default values\n  const defaultValues = options.defaultValues || {}\n  const skipIntrospection = options.skipIntrospection || false\n  const debug = options.debug || false\n\n  // Initialize: Parse query + fetch schema (once)\n  useEffect(() => {\n    async function initialize() {\n      try {\n        if (debug) console.log('[useQueryParams] Initializing...')\n\n        // 1. Extract variables from query AST\n        const queryVariables = extractVariablesFromQuery(query)\n\n        if (debug) {\n          console.log('[useQueryParams] Extracted variables:', queryVariables)\n        }\n\n        // 2. Fetch GraphQL schema via introspection (if needed and not skipped)\n        if (!skipIntrospection && !introspector.isLoaded()) {\n          const endpoint = options.introspectionEndpoint ||\n            (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_API_URL\n              ? `${process.env.NEXT_PUBLIC_API_URL}/graphql`\n              : '')\n\n          if (endpoint) {\n            try {\n              await introspector.fetchSchema(endpoint, options.introspectionHeaders)\n              if (debug) console.log('[useQueryParams] Introspection complete')\n            } catch (err) {\n              console.warn('[useQueryParams] Introspection failed, continuing without it:', err)\n              // Continue without introspection - nested types won't be flattened\n            }\n          }\n        }\n\n        // 3. Flatten schema (with or without introspection)\n        let flattenedSchema = await flattenQueryVariables(queryVariables, introspector)\n\n        // Apply custom param mapping if provided\n        if (options.paramMapping) {\n          flattenedSchema = applyParamMapping(flattenedSchema, options.paramMapping)\n        }\n\n        // Merge default values\n        flattenedSchema = mergeDefaults(flattenedSchema, defaultValues)\n\n        // Validate schema\n        validateSchema(flattenedSchema)\n\n        if (debug) {\n          console.log('[useQueryParams] Flattened schema:', flattenedSchema)\n        }\n\n        setSchema(flattenedSchema)\n        setIsReady(true)\n      } catch (err) {\n        const error = err instanceof Error ? err : new Error(String(err))\n        console.error('[useQueryParams] Initialization failed:', error)\n        setError(error)\n      } finally {\n        setIsLoading(false)\n      }\n    }\n\n    initialize()\n  }, [query, options.introspectionEndpoint, skipIntrospection, debug])\n\n  // Convert URL params to GraphQL variables\n  const variables = useMemo(() => {\n    if (!isReady) {\n      return defaultValues as TVariables\n    }\n\n    try {\n      const varsFromUrl = urlParamsToVariables(searchParams, schema)\n      const merged = { ...defaultValues, ...varsFromUrl }\n\n      if (debug) {\n        console.log('[useQueryParams] Variables from URL:', merged)\n      }\n\n      return merged as TVariables\n    } catch (err) {\n      console.error('[useQueryParams] Failed to convert URL to variables:', err)\n      return defaultValues as TVariables\n    }\n  }, [searchParams, schema, isReady, defaultValues, debug])\n\n  // Raw URL params (before conversion)\n  const params = useMemo(() => {\n    const result: Record<string, any> = {}\n    searchParams.forEach((value, key) => {\n      if (result[key]) {\n        // Multiple values - convert to array\n        if (!Array.isArray(result[key])) {\n          result[key] = [result[key]]\n        }\n        result[key].push(value)\n      } else {\n        result[key] = value\n      }\n    })\n    return result\n  }, [searchParams])\n\n  // Update URL with new parameters\n  const updateUrl = useCallback((newParams: URLSearchParams) => {\n    const url = newParams.toString() ? `?${newParams.toString()}` : window.location.pathname\n\n    if (debug) {\n      console.log('[useQueryParams] Updating URL:', url)\n    }\n\n    // Use replace for shallow routing (no page reload, no history spam)\n    router.replace(url, { scroll: false })\n  }, [router, debug])\n\n  // Set a single parameter\n  const setParam = useCallback((key: string, value: any) => {\n    if (!isReady) {\n      console.warn('[useQueryParams] Schema not ready, cannot set param')\n      return\n    }\n\n    try {\n      const currentVars = variables\n      const updated = mergeVariables(currentVars as Record<string, any>, { [key]: value }, schema)\n      const newParams = variablesToUrlParams(updated, schema)\n      updateUrl(newParams)\n    } catch (err) {\n      console.error('[useQueryParams] Failed to set param:', err)\n    }\n  }, [variables, schema, isReady, updateUrl])\n\n  // Set multiple parameters\n  const setParams = useCallback((updates: Record<string, any>) => {\n    if (!isReady) {\n      console.warn('[useQueryParams] Schema not ready, cannot set params')\n      return\n    }\n\n    try {\n      const currentVars = variables\n      const updated = mergeVariables(currentVars as Record<string, any>, updates, schema)\n      const newParams = variablesToUrlParams(updated, schema)\n      updateUrl(newParams)\n    } catch (err) {\n      console.error('[useQueryParams] Failed to set params:', err)\n    }\n  }, [variables, schema, isReady, updateUrl])\n\n  // Clear specific parameters\n  const clearParamsHandler = useCallback((keys: string[]) => {\n    if (!isReady) {\n      console.warn('[useQueryParams] Schema not ready, cannot clear params')\n      return\n    }\n\n    try {\n      const currentVars = variables\n      const updated = clearParams(currentVars as Record<string, any>, keys, schema)\n      const newParams = variablesToUrlParams(updated, schema)\n      updateUrl(newParams)\n    } catch (err) {\n      console.error('[useQueryParams] Failed to clear params:', err)\n    }\n  }, [variables, schema, isReady, updateUrl])\n\n  // Reset all parameters\n  const resetParams = useCallback(() => {\n    if (debug) {\n      console.log('[useQueryParams] Resetting params')\n    }\n\n    router.replace(window.location.pathname, { scroll: false })\n  }, [router, debug])\n\n  return {\n    variables,\n    params,\n    schema,\n    setParam,\n    setParams,\n    clearParams: clearParamsHandler,\n    resetParams,\n    isReady,\n    isLoading,\n    error\n  }\n}\n\n/**\n * Apply custom parameter name mapping to schema\n */\nfunction applyParamMapping(\n  schema: Record<string, FlattenedParam>,\n  mapping: Record<string, string>\n): Record<string, FlattenedParam> {\n  const mapped: Record<string, FlattenedParam> = {}\n\n  for (const [key, param] of Object.entries(schema)) {\n    const newKey = mapping[key] || key\n    mapped[newKey] = {\n      ...param,\n      urlParamName: mapping[key] || param.urlParamName\n    }\n  }\n\n  return mapped\n}\n","/**\n * GraphQL AST Parser for URL State Management\n *\n * Extracts variable definitions from GraphQL DocumentNode at runtime\n * to automatically generate URL parameter handling.\n */\n\nimport {\n  DocumentNode,\n  VariableDefinitionNode,\n  TypeNode,\n  NamedTypeNode,\n  ListTypeNode,\n  NonNullTypeNode,\n  visit\n} from 'graphql'\n\n/**\n * JavaScript type that can be represented in URL parameters\n */\nexport type JSType = 'string' | 'number' | 'boolean' | 'array' | 'object'\n\n/**\n * Variable definition extracted from GraphQL query\n */\nexport interface VariableDefinition {\n  /** Variable name (e.g., \"search\", \"filter\") */\n  name: string\n  /** JavaScript type for URL parameter handling */\n  type: JSType\n  /** Whether the variable is required (non-null in GraphQL) */\n  required: boolean\n  /** Whether the variable is an array/list */\n  isArray: boolean\n  /** Original GraphQL type name (e.g., \"String\", \"LogFilterInput\") */\n  graphqlTypeName: string\n}\n\n/**\n * Parsed type information from GraphQL TypeNode\n */\ninterface ParsedType {\n  typeName: string\n  isNonNull: boolean\n  isList: boolean\n}\n\n/**\n * Extract all variable definitions from a GraphQL query\n *\n * @param query - GraphQL DocumentNode (from gql template tag)\n * @returns Record of variable definitions keyed by variable name\n *\n * @example\n * const LOGS_QUERY = gql`\n *   query GetLogs($search: String, $filter: LogFilterInput) { ... }\n * `\n *\n * const variables = extractVariablesFromQuery(LOGS_QUERY)\n * // {\n * //   search: { name: 'search', type: 'string', ... },\n * //   filter: { name: 'filter', type: 'object', graphqlTypeName: 'LogFilterInput' }\n * // }\n */\nexport function extractVariablesFromQuery(\n  query: DocumentNode\n): Record<string, VariableDefinition> {\n  const variables: Record<string, VariableDefinition> = {}\n\n  visit(query, {\n    VariableDefinition(node: VariableDefinitionNode) {\n      const name = node.variable.name.value\n      const typeInfo = parseGraphQLType(node.type)\n\n      variables[name] = {\n        name,\n        type: mapGraphQLTypeToJS(typeInfo.typeName),\n        required: typeInfo.isNonNull,\n        isArray: typeInfo.isList,\n        graphqlTypeName: typeInfo.typeName\n      }\n    }\n  })\n\n  return variables\n}\n\n/**\n * Parse GraphQL TypeNode to extract type information\n * Handles NonNullType, ListType, and NamedType recursively\n *\n * @param typeNode - GraphQL type node from AST\n * @returns Parsed type information\n */\nfunction parseGraphQLType(typeNode: TypeNode): ParsedType {\n  let typeName = ''\n  let isNonNull = false\n  let isList = false\n\n  // Unwrap NonNullType wrapper\n  if (typeNode.kind === 'NonNullType') {\n    isNonNull = true\n    typeNode = typeNode.type\n  }\n\n  // Handle ListType\n  if (typeNode.kind === 'ListType') {\n    isList = true\n    typeNode = typeNode.type\n\n    // ListType can also be wrapped in NonNullType\n    if (typeNode.kind === 'NonNullType') {\n      typeNode = typeNode.type\n    }\n  }\n\n  // Extract the base type name\n  if (typeNode.kind === 'NamedType') {\n    typeName = typeNode.name.value\n  }\n\n  return {\n    typeName,\n    isNonNull,\n    isList\n  }\n}\n\n/**\n * Map GraphQL scalar/input types to JavaScript types\n *\n * @param graphqlType - GraphQL type name (e.g., \"String\", \"Int\", \"LogFilterInput\")\n * @returns JavaScript type for URL parameter handling\n */\nfunction mapGraphQLTypeToJS(graphqlType: string): JSType {\n  // GraphQL scalar types\n  const scalarTypeMap: Record<string, JSType> = {\n    'String': 'string',\n    'Int': 'number',\n    'Float': 'number',\n    'Boolean': 'boolean',\n    'ID': 'string'\n  }\n\n  // Check if it's a known scalar\n  if (scalarTypeMap[graphqlType]) {\n    return scalarTypeMap[graphqlType]\n  }\n\n  // Unknown types are assumed to be input objects\n  // These will be flattened using introspection\n  return 'object'\n}\n\n/**\n * Check if a GraphQL type is a scalar type\n */\nexport function isScalarType(graphqlTypeName: string): boolean {\n  const scalars = ['String', 'Int', 'Float', 'Boolean', 'ID']\n  return scalars.includes(graphqlTypeName)\n}\n\n/**\n * Check if a GraphQL type is an input object type\n */\nexport function isInputObjectType(graphqlTypeName: string): boolean {\n  return !isScalarType(graphqlTypeName)\n}\n","/**\n * Schema Flattening Utilities for URL State Management\n *\n * Converts nested GraphQL input types to flat URL parameter mappings.\n * Example: { filter: { severity: [...] } } → URL params: ?severity=...\n */\n\nimport { VariableDefinition, JSType } from './graphql-parser'\nimport { GraphQLIntrospector } from './introspection'\n\n/**\n * Flattened URL parameter configuration\n *\n * Maps URL parameter names to their GraphQL variable paths\n */\nexport interface FlattenedParam {\n  /** URL parameter name (e.g., \"severity\") */\n  urlParamName: string\n  /** Path in GraphQL variables (e.g., \"filter.severity\") */\n  graphqlPath: string\n  /** JavaScript type for URL handling */\n  type: JSType\n  /** Default value for this parameter */\n  defaultValue?: any\n  /** Whether this parameter is required */\n  required?: boolean\n  /** Whether this parameter is an array */\n  isArray?: boolean\n}\n\n/**\n * Flatten GraphQL query variables to URL parameter schema\n *\n * Takes top-level query variables and flattens nested input objects\n * to create a simple URL parameter mapping.\n *\n * @param queryVariables - Variables extracted from GraphQL query\n * @param introspector - Introspector instance with loaded schema\n * @returns Flattened parameter schema for URL handling\n *\n * @example\n * Input variables:\n * {\n *   search: { type: 'string', ... },\n *   filter: { type: 'object', graphqlTypeName: 'LogFilterInput' },\n *   cursor: { type: 'string', ... }\n * }\n *\n * Output schema:\n * {\n *   search: { urlParamName: 'search', graphqlPath: 'search', type: 'string' },\n *   severity: { urlParamName: 'severity', graphqlPath: 'filter.severity', type: 'array' },\n *   toolType: { urlParamName: 'toolType', graphqlPath: 'filter.toolType', type: 'array' },\n *   cursor: { urlParamName: 'cursor', graphqlPath: 'cursor', type: 'string' }\n * }\n *\n * URL: ?search=error&severity=critical&toolType=rmm&cursor=abc\n */\nexport async function flattenQueryVariables(\n  queryVariables: Record<string, VariableDefinition>,\n  introspector: GraphQLIntrospector\n): Promise<Record<string, FlattenedParam>> {\n  const flattened: Record<string, FlattenedParam> = {}\n\n  for (const [varName, varDef] of Object.entries(queryVariables)) {\n    // Primitive types or arrays - keep at top level\n    if (varDef.type !== 'object') {\n      flattened[varName] = {\n        urlParamName: varName,\n        graphqlPath: varName,\n        type: varDef.isArray ? 'array' : varDef.type,\n        required: varDef.required,\n        isArray: varDef.isArray\n      }\n      continue\n    }\n\n    // Input object types - flatten fields to top level\n    // This requires introspection to know the input type's fields\n    if (introspector.isLoaded() && introspector.hasType(varDef.graphqlTypeName)) {\n      const fields = introspector.getInputTypeFields(varDef.graphqlTypeName)\n\n      for (const [fieldName, fieldDef] of Object.entries(fields)) {\n        flattened[fieldName] = {\n          urlParamName: fieldName,\n          graphqlPath: `${varName}.${fieldName}`,\n          type: fieldDef.isArray ? 'array' : fieldDef.type,\n          required: fieldDef.required,\n          isArray: fieldDef.isArray\n        }\n      }\n    } else {\n      // Introspection not available or type not found\n      // Keep as top-level object (will need manual handling)\n      flattened[varName] = {\n        urlParamName: varName,\n        graphqlPath: varName,\n        type: 'object',\n        required: varDef.required,\n        isArray: false\n      }\n    }\n  }\n\n  return flattened\n}\n\n/**\n * Merge default values into flattened schema\n *\n * @param schema - Flattened parameter schema\n * @param defaults - Default values keyed by URL param name\n * @returns Updated schema with default values\n */\nexport function mergeDefaults(\n  schema: Record<string, FlattenedParam>,\n  defaults: Record<string, any>\n): Record<string, FlattenedParam> {\n  const merged: Record<string, FlattenedParam> = {}\n\n  for (const [key, param] of Object.entries(schema)) {\n    merged[key] = {\n      ...param,\n      defaultValue: defaults[key] !== undefined ? defaults[key] : param.defaultValue\n    }\n  }\n\n  return merged\n}\n\n/**\n * Validate that flattened schema has no conflicts\n *\n * Ensures that no two parameters map to the same URL param name\n *\n * @param schema - Flattened parameter schema\n * @throws Error if conflicts are detected\n */\nexport function validateSchema(schema: Record<string, FlattenedParam>): void {\n  const urlParamNames = new Set<string>()\n\n  for (const [key, param] of Object.entries(schema)) {\n    if (urlParamNames.has(param.urlParamName)) {\n      throw new Error(\n        `[FlattenSchema] Conflict: Multiple parameters map to URL param \"${param.urlParamName}\"`\n      )\n    }\n    urlParamNames.add(param.urlParamName)\n  }\n}\n\n/**\n * Get all array-type parameters from schema\n *\n * Useful for knowing which URL params should use repeated values\n * (e.g., ?severity=error&severity=warning)\n *\n * @param schema - Flattened parameter schema\n * @returns Array parameter names\n */\nexport function getArrayParams(schema: Record<string, FlattenedParam>): string[] {\n  return Object.entries(schema)\n    .filter(([_, param]) => param.type === 'array' || param.isArray)\n    .map(([key]) => key)\n}\n\n/**\n * Get required parameters from schema\n *\n * @param schema - Flattened parameter schema\n * @returns Required parameter names\n */\nexport function getRequiredParams(schema: Record<string, FlattenedParam>): string[] {\n  return Object.entries(schema)\n    .filter(([_, param]) => param.required)\n    .map(([key]) => key)\n}\n\n/**\n * Check if a parameter should be included in URL\n *\n * Excludes:\n * - null/undefined values\n * - Empty arrays\n * - Default values (to keep URLs clean)\n *\n * @param value - Parameter value\n * @param param - Parameter configuration\n * @returns Whether to include in URL\n */\nexport function shouldIncludeInUrl(\n  value: any,\n  param: FlattenedParam\n): boolean {\n  // Null/undefined - exclude\n  if (value === null || value === undefined) {\n    return false\n  }\n\n  // Empty arrays - exclude\n  if (Array.isArray(value) && value.length === 0) {\n    return false\n  }\n\n  // Empty strings - exclude\n  if (value === '') {\n    return false\n  }\n\n  // Default values - exclude to keep URL clean\n  if (param.defaultValue !== undefined && value === param.defaultValue) {\n    return false\n  }\n\n  return true\n}\n","/**\n * URL ↔ Variables Converter for URL State Management\n *\n * Bidirectional conversion between URL parameters and GraphQL variables.\n * Handles type coercion, nested paths, and array parameters.\n */\n\nimport { FlattenedParam, shouldIncludeInUrl } from './flatten-schema'\nimport { JSType } from './graphql-parser'\n\n/**\n * Convert URL search params to GraphQL variables\n *\n * Reads URL parameters and reconstructs the nested GraphQL variables object\n * based on the flattened schema mapping.\n *\n * @param searchParams - URLSearchParams from window.location or Next.js\n * @param schema - Flattened parameter schema\n * @returns GraphQL variables object ready for Apollo Client\n *\n * @example\n * URL: ?search=error&severity=critical&severity=error&cursor=abc\n * Schema: {\n *   search: { graphqlPath: 'search', type: 'string' },\n *   severity: { graphqlPath: 'filter.severity', type: 'array' },\n *   cursor: { graphqlPath: 'cursor', type: 'string' }\n * }\n * Result: {\n *   search: 'error',\n *   filter: { severity: ['critical', 'error'] },\n *   cursor: 'abc'\n * }\n */\nexport function urlParamsToVariables(\n  searchParams: URLSearchParams,\n  schema: Record<string, FlattenedParam>\n): Record<string, any> {\n  const variables: Record<string, any> = {}\n\n  for (const [paramName, paramConfig] of Object.entries(schema)) {\n    // Read value from URL\n    const rawValue = paramConfig.type === 'array' || paramConfig.isArray\n      ? searchParams.getAll(paramName)\n      : searchParams.get(paramName)\n\n    // Skip if no value in URL\n    if (!rawValue || (Array.isArray(rawValue) && rawValue.length === 0)) {\n      // Use default value if available\n      if (paramConfig.defaultValue !== undefined) {\n        setNestedValue(variables, paramConfig.graphqlPath, paramConfig.defaultValue)\n      }\n      continue\n    }\n\n    // Coerce value to correct type\n    const value = coerceValue(rawValue, paramConfig.type)\n\n    // Set value at nested path\n    setNestedValue(variables, paramConfig.graphqlPath, value)\n  }\n\n  return variables\n}\n\n/**\n * Convert GraphQL variables to URL search params\n *\n * Flattens nested GraphQL variables to URL parameters based on schema mapping.\n * Excludes null/undefined/default values to keep URLs clean.\n *\n * @param variables - GraphQL variables object\n * @param schema - Flattened parameter schema\n * @returns URLSearchParams ready for router.push()\n *\n * @example\n * Variables: {\n *   search: 'error',\n *   filter: { severity: ['critical'], toolType: [] },\n *   cursor: null\n * }\n * Result URL: ?search=error&severity=critical\n * (empty arrays and null values excluded)\n */\nexport function variablesToUrlParams(\n  variables: Record<string, any>,\n  schema: Record<string, FlattenedParam>\n): URLSearchParams {\n  const params = new URLSearchParams()\n\n  for (const [paramName, paramConfig] of Object.entries(schema)) {\n    // Get value from nested path\n    const value = getNestedValue(variables, paramConfig.graphqlPath)\n\n    // Skip if should not include in URL\n    if (!shouldIncludeInUrl(value, paramConfig)) {\n      continue\n    }\n\n    // Add to URL params\n    if (Array.isArray(value)) {\n      // Array: Use repeated params (e.g., ?tag=foo&tag=bar)\n      value.forEach(v => {\n        if (v !== null && v !== undefined && v !== '') {\n          params.append(paramName, String(v))\n        }\n      })\n    } else {\n      // Single value: Use set\n      params.set(paramName, String(value))\n    }\n  }\n\n  return params\n}\n\n/**\n * Coerce URL parameter value to correct JavaScript type\n *\n * @param value - Raw value from URLSearchParams (string or string[])\n * @param type - Target JavaScript type\n * @returns Coerced value\n */\nexport function coerceValue(value: string | string[], type: JSType): any {\n  // Array handling\n  if (Array.isArray(value)) {\n    return value.map(v => coerceValue(v, type === 'array' ? 'string' : type))\n  }\n\n  // Type coercion for single values\n  switch (type) {\n    case 'number':\n      const num = parseFloat(value)\n      return isNaN(num) ? null : num\n\n    case 'boolean':\n      return value === 'true' || value === '1'\n\n    case 'array':\n      // Single value treated as array with one element\n      return [value]\n\n    case 'string':\n    default:\n      return value\n  }\n}\n\n/**\n * Set value at nested path in object\n *\n * Creates intermediate objects as needed.\n *\n * @param obj - Target object to modify\n * @param path - Dot-separated path (e.g., \"filter.severity\")\n * @param value - Value to set\n *\n * @example\n * setNestedValue({}, 'filter.severity', ['error'])\n * // Result: { filter: { severity: ['error'] } }\n */\nexport function setNestedValue(obj: any, path: string, value: any): void {\n  const parts = path.split('.')\n  let current = obj\n\n  // Navigate/create intermediate objects\n  for (let i = 0; i < parts.length - 1; i++) {\n    const part = parts[i]\n    if (!(part in current) || typeof current[part] !== 'object') {\n      current[part] = {}\n    }\n    current = current[part]\n  }\n\n  // Set final value\n  const lastPart = parts[parts.length - 1]\n  current[lastPart] = value\n}\n\n/**\n * Get value from nested path in object\n *\n * Returns undefined if path doesn't exist.\n *\n * @param obj - Source object\n * @param path - Dot-separated path (e.g., \"filter.severity\")\n * @returns Value at path or undefined\n *\n * @example\n * getNestedValue({ filter: { severity: ['error'] } }, 'filter.severity')\n * // Result: ['error']\n */\nexport function getNestedValue(obj: any, path: string): any {\n  return path.split('.').reduce((current, key) => {\n    return current?.[key]\n  }, obj)\n}\n\n/**\n * Merge URL parameters into existing variables\n *\n * Useful for updating specific parameters without losing others.\n *\n * @param currentVariables - Current GraphQL variables\n * @param updates - Parameter updates (flat object)\n * @param schema - Flattened parameter schema\n * @returns Updated variables object\n */\nexport function mergeVariables(\n  currentVariables: Record<string, any>,\n  updates: Record<string, any>,\n  schema: Record<string, FlattenedParam>\n): Record<string, any> {\n  const merged = { ...currentVariables }\n\n  for (const [paramName, value] of Object.entries(updates)) {\n    const paramConfig = schema[paramName]\n    if (!paramConfig) continue\n\n    setNestedValue(merged, paramConfig.graphqlPath, value)\n  }\n\n  return merged\n}\n\n/**\n * Clear specific parameters from variables\n *\n * @param variables - Current GraphQL variables\n * @param paramNames - Parameter names to clear\n * @param schema - Flattened parameter schema\n * @returns Variables with specified params removed\n */\nexport function clearParams(\n  variables: Record<string, any>,\n  paramNames: string[],\n  schema: Record<string, FlattenedParam>\n): Record<string, any> {\n  const cleared = { ...variables }\n\n  for (const paramName of paramNames) {\n    const paramConfig = schema[paramName]\n    if (!paramConfig) continue\n\n    // Set to undefined (will be excluded from URL)\n    setNestedValue(cleared, paramConfig.graphqlPath, undefined)\n  }\n\n  return cleared\n}\n\n/**\n * Validate GraphQL variables against schema\n *\n * Checks for required parameters and type consistency.\n *\n * @param variables - GraphQL variables to validate\n * @param schema - Flattened parameter schema\n * @returns Validation errors (empty array if valid)\n */\nexport function validateVariables(\n  variables: Record<string, any>,\n  schema: Record<string, FlattenedParam>\n): string[] {\n  const errors: string[] = []\n\n  for (const [paramName, paramConfig] of Object.entries(schema)) {\n    const value = getNestedValue(variables, paramConfig.graphqlPath)\n\n    // Check required parameters\n    if (paramConfig.required && (value === null || value === undefined)) {\n      errors.push(`Required parameter \"${paramName}\" is missing`)\n    }\n\n    // Check type consistency\n    if (value !== null && value !== undefined) {\n      const actualType = Array.isArray(value) ? 'array' : typeof value\n      const expectedType = paramConfig.type === 'array' ? 'array' : paramConfig.type\n\n      if (actualType !== expectedType && actualType !== 'object') {\n        errors.push(\n          `Parameter \"${paramName}\" has wrong type: expected ${expectedType}, got ${actualType}`\n        )\n      }\n    }\n  }\n\n  return errors\n}\n","/**\n * GraphQL Schema Introspection for URL State Management\n *\n * Fetches GraphQL schema via introspection query at runtime (works behind auth).\n * Caches schema in localStorage + memory for performance.\n */\n\nimport {\n  getIntrospectionQuery,\n  buildClientSchema,\n  IntrospectionQuery,\n  GraphQLSchema,\n  GraphQLInputObjectType,\n  GraphQLInputType,\n  isInputObjectType\n} from 'graphql'\nimport { VariableDefinition, JSType } from './graphql-parser'\n\n/**\n * Schema cache structure stored in localStorage\n */\ninterface SchemaCache {\n  schema: IntrospectionQuery\n  timestamp: number\n  version: string\n}\n\n/**\n * GraphQL Introspection Manager\n *\n * Singleton class that handles schema introspection with caching.\n * Fetches schema once per session and caches for 24 hours.\n */\nexport class GraphQLIntrospector {\n  private schema: GraphQLSchema | null = null\n  private cacheKey = 'graphql-schema-cache-v1'\n  private cacheDuration = 24 * 60 * 60 * 1000  // 24 hours\n  private schemaVersion = '1.0.0'\n\n  /**\n   * Fetch GraphQL schema via introspection query\n   *\n   * @param endpoint - GraphQL endpoint URL\n   * @param headers - HTTP headers (e.g., authentication)\n   * @param skipCache - Force fresh fetch, ignore cache\n   *\n   * @example\n   * await introspector.fetchSchema(\n   *   'http://localhost/api/graphql',\n   *   { 'Authorization': 'Bearer token123' }\n   * )\n   */\n  async fetchSchema(\n    endpoint: string,\n    headers: Record<string, string> = {},\n    skipCache = false\n  ): Promise<void> {\n    // Check cache first (unless skipped)\n    if (!skipCache) {\n      const cached = this.loadFromCache()\n      if (cached && Date.now() - cached.timestamp < this.cacheDuration) {\n        try {\n          this.schema = buildClientSchema(cached.schema)\n          return\n        } catch (error) {\n          console.warn('[Introspector] Failed to build schema from cache:', error)\n          // Continue to fetch fresh schema\n        }\n      }\n    }\n\n    // Fetch schema via introspection query\n    try {\n      const response = await fetch(endpoint, {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n          ...headers\n        },\n        body: JSON.stringify({\n          query: getIntrospectionQuery()\n        })\n      })\n\n      if (!response.ok) {\n        throw new Error(`HTTP ${response.status}: ${response.statusText}`)\n      }\n\n      const { data, errors } = await response.json()\n\n      if (errors) {\n        throw new Error(`GraphQL errors: ${JSON.stringify(errors)}`)\n      }\n\n      if (!data) {\n        throw new Error('No data returned from introspection query')\n      }\n\n      // Build and cache schema\n      this.schema = buildClientSchema(data)\n      this.saveToCache(data)\n    } catch (error) {\n      console.error('[Introspector] Failed to fetch schema:', error)\n      throw error\n    }\n  }\n\n  /**\n   * Get fields from a GraphQL input object type\n   *\n   * @param typeName - Name of the input type (e.g., \"LogFilterInput\")\n   * @returns Record of field definitions\n   *\n   * @example\n   * const fields = introspector.getInputTypeFields('LogFilterInput')\n   * // {\n   * //   severity: { name: 'severity', type: 'array', ... },\n   * //   toolType: { name: 'toolType', type: 'array', ... }\n   * // }\n   */\n  getInputTypeFields(typeName: string): Record<string, VariableDefinition> {\n    if (!this.schema) {\n      return {}\n    }\n\n    const type = this.schema.getType(typeName)\n\n    if (!type || !isInputObjectType(type)) {\n      return {}\n    }\n\n    const fields: Record<string, VariableDefinition> = {}\n    const typeFields = type.getFields()\n\n    for (const [fieldName, field] of Object.entries(typeFields)) {\n      const fieldType = this.unwrapType(field.type)\n\n      fields[fieldName] = {\n        name: fieldName,\n        type: this.mapGraphQLTypeToJS(fieldType.typeName),\n        required: fieldType.isNonNull,\n        isArray: fieldType.isList,\n        graphqlTypeName: fieldType.typeName\n      }\n    }\n\n    return fields\n  }\n\n  /**\n   * Check if a type exists in the schema\n   */\n  hasType(typeName: string): boolean {\n    if (!this.schema) return false\n    return this.schema.getType(typeName) !== undefined\n  }\n\n  /**\n   * Get the schema (if loaded)\n   */\n  getSchema(): GraphQLSchema | null {\n    return this.schema\n  }\n\n  /**\n   * Check if schema is loaded\n   */\n  isLoaded(): boolean {\n    return this.schema !== null\n  }\n\n  /**\n   * Clear cached schema\n   */\n  clearCache(): void {\n    try {\n      localStorage.removeItem(this.cacheKey)\n      this.schema = null\n    } catch {\n      // Ignore storage errors\n    }\n  }\n\n  /**\n   * Unwrap GraphQL type to get base type info\n   * Handles NonNull and List wrappers\n   */\n  private unwrapType(type: GraphQLInputType): {\n    typeName: string\n    isNonNull: boolean\n    isList: boolean\n  } {\n    let typeName = ''\n    let isNonNull = false\n    let isList = false\n    let current: any = type\n\n    // Unwrap NonNull wrapper\n    if (current.toString().endsWith('!')) {\n      isNonNull = true\n      current = 'ofType' in current ? current.ofType : current\n    }\n\n    // Check for List wrapper\n    if (current.toString().startsWith('[')) {\n      isList = true\n      current = 'ofType' in current ? current.ofType : current\n    }\n\n    // Handle inner NonNull (e.g., [String!])\n    if (current.toString().endsWith('!')) {\n      current = 'ofType' in current ? current.ofType : current\n    }\n\n    // Get base type name\n    typeName = current.name || current.toString().replace(/[[\\]!]/g, '')\n\n    return { typeName, isNonNull, isList }\n  }\n\n  /**\n   * Map GraphQL types to JavaScript types for URL handling\n   */\n  private mapGraphQLTypeToJS(graphqlType: string): JSType {\n    const typeMap: Record<string, JSType> = {\n      'String': 'string',\n      'Int': 'number',\n      'Float': 'number',\n      'Boolean': 'boolean',\n      'ID': 'string'\n    }\n\n    return typeMap[graphqlType] || 'object'\n  }\n\n  /**\n   * Load schema from localStorage cache\n   */\n  private loadFromCache(): SchemaCache | null {\n    try {\n      const cached = localStorage.getItem(this.cacheKey)\n      if (!cached) return null\n\n      const parsed = JSON.parse(cached) as SchemaCache\n\n      // Validate cache version\n      if (parsed.version !== this.schemaVersion) {\n        return null\n      }\n\n      return parsed\n    } catch (error) {\n      console.warn('[Introspector] Failed to load cache:', error)\n      return null\n    }\n  }\n\n  /**\n   * Save schema to localStorage cache\n   */\n  private saveToCache(schema: IntrospectionQuery): void {\n    try {\n      const cache: SchemaCache = {\n        schema,\n        timestamp: Date.now(),\n        version: this.schemaVersion\n      }\n\n      localStorage.setItem(this.cacheKey, JSON.stringify(cache))\n    } catch (error) {\n      console.warn('[Introspector] Failed to save cache:', error)\n      // Ignore storage errors (e.g., quota exceeded)\n    }\n  }\n}\n\n/**\n * Singleton introspector instance\n *\n * Use this instance throughout the application to share schema cache\n *\n * @example\n * import { introspector } from '@flamingo/ui-kit/hooks/state'\n *\n * // Initialize after auth\n * await introspector.fetchSchema(endpoint, { Authorization: token })\n *\n * // Get input type fields\n * const fields = introspector.getInputTypeFields('LogFilterInput')\n */\nexport const introspector = new GraphQLIntrospector()\n","/**\n * useApiParams Hook - REST API Integration for URL State Management\n *\n * Manual schema definition for REST APIs. Provides same URL sync functionality\n * as useQueryParams but without GraphQL dependency.\n *\n * @example\n * const { params, setParam } = useApiParams({\n *   search: { type: 'string', default: '' },\n *   page: { type: 'number', default: 1 },\n *   tags: { type: 'array', default: [] }\n * })\n *\n * fetch(`/api/items?${new URLSearchParams(params)}`)\n *\n * // URL: /items?search=laptop&page=2&tags=electronics&tags=sale\n * // params: { search: 'laptop', page: 2, tags: ['electronics', 'sale'] }\n */\n\n'use client'\n\nimport { useRouter, useSearchParams } from '../../embed-shims/next-navigation'\nimport { useCallback, useMemo, useRef } from 'react'\nimport { FlattenedParam, shouldIncludeInUrl } from './flatten-schema'\nimport { JSType } from './graphql-parser'\nimport { coerceValue } from './url-converter'\n\n/**\n * Returns the previous reference if the JSON-serialized content of `value`\n * hasn't changed across renders. Internal helper used to shield consumers from\n * ref churn caused by:\n *   - `useSearchParams()` returning a new `ReadonlyURLSearchParams` instance\n *     on every render even when the URL is unchanged (Next.js behavior).\n *   - Consumers passing the schema as a fresh object literal on every render.\n */\nfunction useContentStable<T>(value: T, key: string): T {\n  const ref = useRef<{ value: T; key: string } | undefined>(undefined)\n  if (ref.current && ref.current.key === key) return ref.current.value\n  ref.current = { value, key }\n  return value\n}\n\n/**\n * Reuses a previous array reference if its content (shallow string equality)\n * matches the freshly parsed array. Lets `params.tier` etc. stay\n * reference-stable across renders that don't actually change those values.\n */\nfunction reuseIfShallowEqual<T extends string | number | boolean>(\n  prev: unknown,\n  next: T[],\n): T[] {\n  if (!Array.isArray(prev) || prev.length !== next.length) return next\n  for (let i = 0; i < next.length; i++) {\n    if (prev[i] !== next[i]) return next\n  }\n  return prev as T[]\n}\n\n/**\n * Type mapping from JSType to TypeScript types for OUTPUT (reading params)\n */\ntype OutputTypeMap = {\n  string: string\n  number: number\n  boolean: boolean\n  array: string[]\n  object: Record<string, unknown>\n}\n\n/**\n * Type mapping from JSType to TypeScript types for INPUT (setting params)\n * More permissive to allow null/undefined in arrays which get filtered\n */\ntype InputTypeMap = {\n  string: string | null | undefined\n  number: number | null | undefined\n  boolean: boolean | null | undefined\n  array: (string | null | undefined)[]\n  object: Record<string, unknown> | null | undefined\n}\n\n/**\n * Get the TypeScript type for OUTPUT (reading from params)\n */\ntype OutputTypeForJSType<T extends JSType> = OutputTypeMap[T]\n\n/**\n * Get the TypeScript type for INPUT (setting params)\n */\ntype InputTypeForJSType<T extends JSType> = InputTypeMap[T]\n\n/**\n * Get the default value type for a given JSType\n */\ntype DefaultValueForType<T extends JSType> =\n  T extends 'array' ? string[] :\n  T extends 'object' ? Record<string, unknown> :\n  OutputTypeMap[T]\n\n/**\n * Parameter configuration for a single parameter\n */\nexport interface ParamConfig<T extends JSType = JSType> {\n  /** JavaScript type for URL parameter */\n  type: T\n  /** Default value matching the type */\n  default?: DefaultValueForType<T>\n  /** Whether parameter is required */\n  required?: boolean\n}\n\n/**\n * REST API parameter schema definition\n * Maps parameter names to their configuration\n */\nexport type ParamSchema = Record<string, ParamConfig>\n\n/**\n * Helper to create a typed param schema (preserves literal types)\n */\nexport function defineParamSchema<T extends ParamSchema>(schema: T): T {\n  return schema\n}\n\n/**\n * Options for useApiParams hook\n */\nexport interface UseApiParamsOptions {\n  /** Enable debug logging */\n  debug?: boolean\n}\n\n/**\n * Infer the OUTPUT params type from a ParamSchema (for reading)\n * Maps each key in the schema to its corresponding TypeScript type\n *\n * @example\n * const schema = defineParamSchema({\n *   search: { type: 'string', default: '' },\n *   page: { type: 'number', default: 1 },\n *   tags: { type: 'array', default: [] }\n * })\n * type Params = InferParamsFromSchema<typeof schema>\n * // { search: string; page: number; tags: string[] }\n */\nexport type InferParamsFromSchema<TSchema extends ParamSchema> = {\n  [K in keyof TSchema]: TSchema[K]['type'] extends infer T\n    ? T extends JSType\n      ? OutputTypeForJSType<T>\n      : never\n    : never\n}\n\n/**\n * Infer the INPUT params type from a ParamSchema (for setting)\n * More permissive to allow null/undefined values\n */\nexport type InferInputParamsFromSchema<TSchema extends ParamSchema> = {\n  [K in keyof TSchema]: TSchema[K]['type'] extends infer T\n    ? T extends JSType\n      ? InputTypeForJSType<T>\n      : never\n    : never\n}\n\n/**\n * Type for parameter values that can be set\n * Allows setting values that match the schema types or can be coerced to them\n */\nexport type ParamValue = \n  | string \n  | number \n  | boolean \n  | string[] \n  | (string | null | undefined)[]\n  | Record<string, unknown> \n  | null \n  | undefined\n\n/**\n * Return type for useApiParams hook with strict typing\n */\nexport interface UseApiParamsReturn<\n  TSchema extends ParamSchema,\n  TParams = InferParamsFromSchema<TSchema>\n> {\n  /** Parsed parameters object with strict typing */\n  params: TParams\n\n  /** URLSearchParams for fetch/axios */\n  urlSearchParams: URLSearchParams\n\n  /** Set a single parameter with type-safe key and value */\n  setParam: <K extends keyof TSchema & string>(\n    key: K,\n    value: InferInputParamsFromSchema<Pick<TSchema, K>>[K]\n  ) => void\n\n  /** Set multiple parameters at once */\n  setParams: (updates: Partial<InferInputParamsFromSchema<TSchema>>) => void\n\n  /** Clear specific parameters */\n  clearParams: (keys: (keyof TSchema & string)[]) => void\n\n  /** Reset all parameters (clear URL) */\n  resetParams: () => void\n}\n\n/**\n * useApiParams - Manual URL state for REST APIs\n *\n * This hook:\n * 1. Reads URL search parameters\n * 2. Coerces to correct types based on schema\n * 3. Provides type-safe parameter updates\n * 4. Syncs changes to URL automatically\n *\n * @param schema - Parameter schema definition\n * @param options - Configuration options\n * @returns Hook API for managing URL state\n */\nexport function useApiParams<TSchema extends ParamSchema>(\n  schema: TSchema,\n  options: UseApiParamsOptions = {}\n): UseApiParamsReturn<TSchema> {\n  const router = useRouter()\n  const searchParamsLive = useSearchParams()\n  const debug = options.debug || false\n\n  // ───── Reference-stability layer ──────────────────────────────────────\n  //\n  // Goal: `params`, `params.<arrayField>`, and the setter callbacks must keep\n  // the SAME reference across renders unless the URL or schema content\n  // actually changes. Otherwise consumer `useMemo`/`useEffect` deps that\n  // include `params.foo` invalidate on every parent re-render.\n  //\n  // Without this, every call site has to defensively `JSON.stringify` filter\n  // arrays into a content-key — a known footgun. The stability is provided\n  // here, once, instead of in 17 consumers.\n\n  // 1. URL string is the canonical, value-stable representation of search params.\n  const searchString = searchParamsLive.toString()\n\n  // 2. Schema reference stabilized by content. Consumers commonly pass an\n  //    object literal each render, which would otherwise invalidate every memo.\n  const schemaKey = useMemo(() => JSON.stringify(schema), [schema])\n  const stableSchema = useContentStable(schema, schemaKey)\n\n  // ──────────────────────────────────────────────────────────────────────\n\n  // Convert schema to flattened format for reuse\n  // biome-ignore lint/correctness/useExhaustiveDependencies: schemaKey is the content-stable key for `stableSchema`.\n  const flattenedSchema = useMemo((): Record<string, FlattenedParam> => {\n    const flattened: Record<string, FlattenedParam> = {}\n\n    for (const [key, config] of Object.entries(stableSchema)) {\n      flattened[key] = {\n        urlParamName: key,\n        graphqlPath: key,\n        type: config.type,\n        defaultValue: config.default,\n        required: config.required,\n        isArray: config.type === 'array'\n      }\n    }\n\n    return flattened\n  }, [schemaKey])\n\n  // Parse URL parameters with type coercion. Reuse previous array refs when\n  // their content is unchanged so `params.<arrayField>` stays stable across\n  // renders that don't touch that specific field.\n  const prevParamsRef = useRef<Record<string, unknown> | undefined>(undefined)\n  // biome-ignore lint/correctness/useExhaustiveDependencies: `searchString` and `schemaKey` are content-stable representations of `searchParamsLive` and `stableSchema`.\n  const params = useMemo((): InferParamsFromSchema<TSchema> => {\n    const sp = new URLSearchParams(searchString)\n    const result: Record<string, unknown> = {}\n    const prev = prevParamsRef.current\n\n    for (const [key, config] of Object.entries(stableSchema)) {\n      // Read from URL\n      const rawValue = config.type === 'array'\n        ? sp.getAll(key)\n        : sp.get(key)\n\n      // Use value from URL or default\n      let value: unknown\n      if (rawValue && (Array.isArray(rawValue) ? rawValue.length > 0 : true)) {\n        value = coerceValue(rawValue, config.type)\n      } else {\n        value = config.default\n      }\n\n      // Reuse previous reference when content matches — keeps array fields\n      // reference-stable when an unrelated param changed.\n      if (Array.isArray(value) && prev) {\n        value = reuseIfShallowEqual(prev[key], value as (string | number | boolean)[])\n      }\n\n      result[key] = value\n    }\n\n    if (debug) {\n      console.log('[useApiParams] Parsed params:', result)\n    }\n\n    prevParamsRef.current = result\n    return result as InferParamsFromSchema<TSchema>\n  }, [searchString, schemaKey, debug])\n\n  // Helper: Add parameter value to URLSearchParams\n  const addParamToSearchParams = useCallback((\n    searchParams: URLSearchParams,\n    key: string,\n    value: ParamValue\n  ): void => {\n    if (value === undefined || value === '' || value === null) {\n      return\n    }\n\n    if (Array.isArray(value)) {\n      value.forEach(v => {\n        if (v !== undefined && v !== '' && v !== null) {\n          searchParams.append(key, String(v))\n        }\n      })\n    } else if (typeof value === 'object') {\n      // For objects, convert to JSON string\n      searchParams.set(key, JSON.stringify(value))\n    } else {\n      searchParams.set(key, String(value))\n    }\n  }, [])\n\n  // Get URLSearchParams for fetch/axios. Iterates `stableSchema` (not raw\n  // `schema`) so consumers passing an inline schema literal don't invalidate\n  // this memo on every render.\n  // biome-ignore lint/correctness/useExhaustiveDependencies: `schemaKey` is the content-stable key for `stableSchema`.\n  const urlSearchParams = useMemo((): URLSearchParams => {\n    const newParams = new URLSearchParams()\n\n    for (const key of Object.keys(stableSchema)) {\n      const value = (params as Record<string, unknown>)[key]\n      const paramConfig = flattenedSchema[key]\n\n      // Skip if should not include\n      if (!shouldIncludeInUrl(value, paramConfig)) {\n        continue\n      }\n\n      addParamToSearchParams(newParams, key, value as ParamValue)\n    }\n\n    return newParams\n  }, [params, schemaKey, flattenedSchema, addParamToSearchParams])\n\n  // Update URL with new parameters (preserve other params not managed by this\n  // hook). Depends only on value-stable inputs (`searchString`, `schemaKey`),\n  // so the callback ref itself is stable across renders that don't change URL\n  // or schema — important for consumers that put `setParam`/`setParams` in\n  // `useEffect` deps.\n  // biome-ignore lint/correctness/useExhaustiveDependencies: `searchString` and `schemaKey` are content-stable representations of `searchParamsLive` and `stableSchema`.\n  const updateUrl = useCallback((newParams: URLSearchParams, keysToRemove: string[] = []) => {\n    // Preserve all existing params, then override with new ones\n    const finalParams = new URLSearchParams(searchString)\n\n    // Remove keys that are explicitly marked for removal\n    keysToRemove.forEach(key => {\n      if (key in stableSchema) {\n        finalParams.delete(key)\n      }\n    })\n\n    // Remove keys that are being updated (from newParams)\n    // This preserves other schema parameters that aren't being changed\n    newParams.forEach((_, key) => {\n      // Only remove keys that are in our schema\n      if (key in stableSchema) {\n        finalParams.delete(key)\n      }\n    })\n\n    // Add all new values (including multiple values for array params)\n    // Only add parameters that are in our schema to avoid duplicating external params\n    newParams.forEach((value, key) => {\n      // Only process keys that are in our schema\n      if (key in stableSchema) {\n        if (finalParams.has(key)) {\n          // Key already exists (from array params), append\n          finalParams.append(key, value)\n        } else {\n          // First value for this key, use set\n          finalParams.set(key, value)\n        }\n      }\n    })\n\n    const url = finalParams.toString()\n      ? `?${finalParams.toString()}`\n      : window.location.pathname\n\n    if (debug) {\n      console.log('[useApiParams] Updating URL:', url)\n    }\n\n    // Use replace for shallow routing (no page reload, no history spam)\n    router.replace(url, { scroll: false })\n  }, [router, debug, searchString, schemaKey])\n\n  // Helper to check if value is empty\n  const isEmptyValue = (value: unknown): boolean => {\n    if (value === undefined || value === null || value === '') {\n      return true\n    }\n    if (Array.isArray(value)) {\n      // Empty array or array with all empty/null/undefined values\n      return value.length === 0 || value.every(v => v === undefined || v === null || v === '')\n    }\n    return false\n  }\n\n  // Set a single parameter\n  // biome-ignore lint/correctness/useExhaustiveDependencies: `schemaKey` is the content-stable key for `stableSchema`.\n  const setParam = useCallback(<K extends keyof TSchema & string>(\n    key: K,\n    value: InferInputParamsFromSchema<Pick<TSchema, K>>[K]\n  ) => {\n    const config = stableSchema[key]\n\n    if (!config) {\n      console.warn(`[useApiParams] Unknown parameter: ${key}`)\n      return\n    }\n\n    const newParams = new URLSearchParams()\n\n    if (isEmptyValue(value)) {\n      updateUrl(newParams, [key])\n    } else {\n      addParamToSearchParams(newParams, key, value as ParamValue)\n      updateUrl(newParams)\n    }\n  }, [schemaKey, updateUrl, addParamToSearchParams])\n\n  // Set multiple parameters\n  // biome-ignore lint/correctness/useExhaustiveDependencies: `schemaKey` is the content-stable key for `stableSchema`.\n  const setParams = useCallback((\n    updates: Partial<InferInputParamsFromSchema<TSchema>>\n  ) => {\n    const newParams = new URLSearchParams()\n    const keysToRemove: string[] = []\n\n    for (const [key, value] of Object.entries(updates)) {\n      const config = stableSchema[key]\n\n      if (!config) {\n        console.warn(`[useApiParams] Unknown parameter: ${key}`)\n        continue\n      }\n\n      if (isEmptyValue(value)) {\n        keysToRemove.push(key)\n      } else {\n        addParamToSearchParams(newParams, key, value as ParamValue)\n      }\n    }\n\n    updateUrl(newParams, keysToRemove)\n  }, [schemaKey, updateUrl, addParamToSearchParams])\n\n  // Clear specific parameters\n  const clearParams = useCallback((keys: (keyof TSchema & string)[]) => {\n    const newParams = new URLSearchParams()\n    updateUrl(newParams, keys)\n  }, [updateUrl])\n\n  // Reset all parameters\n  const resetParams = useCallback(() => {\n    if (debug) {\n      console.log('[useApiParams] Resetting params')\n    }\n\n    router.replace(window.location.pathname, { scroll: false })\n  }, [router, debug])\n\n  return {\n    params,\n    urlSearchParams,\n    setParam,\n    setParams,\n    clearParams,\n    resetParams\n  }\n}\n\n/**\n * Helper: Create URLSearchParams from object\n *\n * Handles arrays as repeated parameters. Filters out undefined, and empty values.\n *\n * @param params - Parameters object\n * @returns URLSearchParams\n */\nexport function createSearchParams(params: Record<string, ParamValue>): URLSearchParams {\n  const searchParams = new URLSearchParams()\n\n  for (const [key, value] of Object.entries(params)) {\n    if (value === undefined || value === '' || value === null) {\n      continue\n    }\n\n    if (Array.isArray(value)) {\n      value.forEach(v => {\n        if (v !== undefined && v !== '' && v !== null) {\n          searchParams.append(key, String(v))\n        }\n      })\n    } else if (typeof value === 'object') {\n      // For objects, convert to JSON string\n      searchParams.set(key, JSON.stringify(value))\n    } else {\n      searchParams.set(key, String(value))\n    }\n  }\n\n  return searchParams\n}","/**\n * Unified Cursor Pagination State Hook\n *\n * Manages all common cursor-based pagination state logic:\n * - URL state management with useApiParams\n * - Debounced search input\n * - hasLoadedBeyondFirst tracking\n * - Initial load detection\n * - Search change detection\n * - Pagination handlers (next, reset)\n *\n * This eliminates ~60-80 lines of boilerplate from each paginated component.\n *\n * @example\n * const {\n *   searchInput, setSearchInput,\n *   hasLoadedBeyondFirst,\n *   handleNextPage, handleResetToFirstPage\n * } = useCursorPaginationState({\n *   onInitialLoad: (search, cursor) => fetchDialogs(false, search, true, cursor),\n *   onSearchChange: (search) => fetchDialogs(false, search)\n * })\n */\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { useApiParams, type UseApiParamsReturn } from './use-api-params';\n\nexport interface UseCursorPaginationStateOptions {\n  /**\n   * Debounce delay for search input (default: 300ms)\n   */\n  debounceMs?: number\n\n  /**\n   * Callback for initial page load\n   * Called once on mount with current search and cursor from URL\n   */\n  onInitialLoad: (search: string, cursor: string | null) => void | Promise<unknown>\n\n  /**\n   * Callback when search term changes (after debounce)\n   * Called after URL is updated, cursor is already reset\n   */\n  onSearchChange: (search: string) => void | Promise<unknown>\n}\n\nconst urlSchema = {\n  search: { type: 'string' as const, default: '' },\n  cursor: { type: 'string' as const, default: '' },\n}\n\ntype ApiParamsReturn = UseApiParamsReturn<typeof urlSchema>\n\n/** Pagination params managed by this hook */\nexport type PaginationParams = ApiParamsReturn['params']\n\nexport interface CursorPaginationStateReturn {\n  // Search\n  searchInput: string\n  setSearchInput: (value: string) => void\n\n  // Pagination tracking\n  hasLoadedBeyondFirst: boolean\n  setHasLoadedBeyondFirst: (value: boolean) => void\n\n  // Handlers for useTablePagination\n  handleNextPage: (endCursor: string, fetchFn: () => Promise<unknown>) => Promise<void>\n  handleResetToFirstPage: (fetchFn: () => Promise<unknown>) => Promise<void>\n\n  // URL params access (for advanced use cases)\n  params: PaginationParams\n  setParam: ApiParamsReturn['setParam']\n  setParams: ApiParamsReturn['setParams']\n}\n\nexport function useCursorPaginationState(\n  options: UseCursorPaginationStateOptions\n): CursorPaginationStateReturn {\n  const {\n    onInitialLoad,\n    onSearchChange,\n  } = options\n\n  const { params, setParam, setParams } = useApiParams(urlSchema)\n\n  // Local search input with debounce\n  const [searchInput, setSearchInput] = useState(params.search || '')\n\n  // Pagination tracking\n  const [hasLoadedBeyondFirst, setHasLoadedBeyondFirst] = useState(false)\n  // Use a counter instead of boolean to ensure effects see the latest state\n  const [initialLoadCount, setInitialLoadCount] = useState(0)\n  // Initialize to null to distinguish \"never set\" from \"set to empty string\"\n  const lastSearchRef = useRef<string | null>(null)\n  // Track if we're syncing from URL to prevent loops\n  const isSyncingFromUrl = useRef(false)\n  // Track if initial load is in progress to block ALL other effects\n  const isInitialLoadInProgress = useRef(true)\n\n  // Sync local input with URL param (for tab switches that clear params)\n  // Only sync if the URL changed externally (not from our own debounce update)\n  useEffect(() => {\n    // Block during initial load\n    if (isInitialLoadInProgress.current) return\n\n    const urlSearch = params.search || ''\n    // Only sync if URL differs from current input\n    if (urlSearch !== searchInput) {\n      isSyncingFromUrl.current = true\n      setSearchInput(urlSearch)\n      // Reset the flag after a tick to allow normal operation\n      setTimeout(() => { isSyncingFromUrl.current = false }, 0)\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [params.search, initialLoadCount]) // Add initialLoadCount to re-run after initial load\n\n  // Sync debounced search to URL, reset cursor when search changes\n  useEffect(() => {\n    // Block during initial load\n    if (isInitialLoadInProgress.current) return\n    // Skip if we're syncing from URL to prevent loops\n    if (isSyncingFromUrl.current) return\n\n    if (searchInput !== params.search) {\n      setParams({\n        search: searchInput,\n        cursor: '' // Reset cursor when search changes\n      })\n    }\n  }, [searchInput, params.search, setParams, initialLoadCount])\n\n  // Initial load effect - runs once and blocks all other effects until complete\n  useEffect(() => {\n    if (initialLoadCount === 0) {\n      const cursor = params.cursor || null\n      const search = params.search || ''\n\n      // Set all refs BEFORE calling onInitialLoad\n      lastSearchRef.current = search\n\n      // If we have a cursor in URL (page refresh), we're beyond first page\n      if (cursor) {\n        setHasLoadedBeyondFirst(true)\n      }\n\n      // Call the initial load and wait for it to complete before\n      // marking initial load as done (handles async onInitialLoad)\n      Promise.resolve(onInitialLoad(search, cursor)).finally(() => {\n        isInitialLoadInProgress.current = false\n        setInitialLoadCount(1)\n      })\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [])\n\n  // Search change detection - only after initial load is fully complete\n  useEffect(() => {\n    // Block during initial load\n    if (isInitialLoadInProgress.current) return\n    if (initialLoadCount === 0) return\n\n    const currentSearch = params.search || ''\n    // Only trigger search change if:\n    // 1. lastSearchRef has been set (not null - means initial load happened)\n    // 2. The search actually changed\n    if (lastSearchRef.current !== null && currentSearch !== lastSearchRef.current) {\n      lastSearchRef.current = currentSearch\n      setHasLoadedBeyondFirst(false)\n      onSearchChange(currentSearch)\n    }\n  }, [params.search, onSearchChange, initialLoadCount])\n\n  // Pagination handlers\n  const handleNextPage = useCallback(\n    async (endCursor: string, fetchFn: () => Promise<unknown>) => {\n      setParam('cursor', endCursor)\n      await fetchFn()\n      setHasLoadedBeyondFirst(true)\n    },\n    [setParam]\n  )\n\n  const handleResetToFirstPage = useCallback(\n    async (fetchFn: () => Promise<unknown>) => {\n      setParam('cursor', '')\n      await fetchFn()\n      setHasLoadedBeyondFirst(false)\n    },\n    [setParam]\n  )\n\n  return {\n    searchInput,\n    setSearchInput,\n    hasLoadedBeyondFirst,\n    setHasLoadedBeyondFirst,\n    handleNextPage,\n    handleResetToFirstPage,\n    params,\n    setParam,\n    setParams,\n  }\n}\n","import { useEffect, useMemo, useState } from 'react'\n\nimport type { NatsClient, NatsClientOptions, NatsStatus } from '../../nats'\nimport { createNatsClient } from '../../nats'\n\nexport interface UseNatsClientOptions {\n  /**\n   * When true, connect on mount and close on unmount.\n   */\n  autoConnect?: boolean\n}\n\nexport interface UseNatsClientResult {\n  client: NatsClient | null\n  status: NatsStatus\n  isConnected: boolean\n  lastError: unknown\n}\n\n/**\n * React hook for managing a shared NATS WebSocket connection lifecycle.\n *\n * Important: pass a memoized `clientOptions` (e.g. via `useMemo`) to avoid\n * reconnecting on every render.\n */\nexport function useNatsClient(\n  clientOptions: NatsClientOptions | null,\n  options: UseNatsClientOptions = {},\n): UseNatsClientResult {\n  const { autoConnect = true } = options\n\n  const client = useMemo(() => {\n    if (!clientOptions) return null\n    return createNatsClient(clientOptions)\n  }, [clientOptions])\n\n  const [status, setStatus] = useState<NatsStatus>('disconnected')\n  const [lastError, setLastError] = useState<unknown>(null)\n\n  useEffect(() => {\n    if (!client) {\n      setStatus('disconnected')\n      setLastError(null)\n      return\n    }\n\n    const off = client.onStatus((event) => {\n      setStatus(event.status)\n      if (event.status === 'error') setLastError(event.data)\n    })\n\n    if (autoConnect) {\n      client.connect().catch((e) => {\n        setLastError(e)\n        setStatus('error')\n      })\n    }\n\n    return () => {\n      off()\n      client.close().catch(() => {\n        // ignore\n      })\n    }\n  }, [client, autoConnect])\n\n  return {\n    client,\n    status,\n    isConnected: status === 'connected',\n    lastError,\n  }\n}\n","/**\n * useNearViewport — module-level shared IntersectionObserver in hook form.\n *\n * Single IO instance per `rootMargin` value, shared across every component\n * that mounts the hook. Reduces overhead vs. one IO per component on\n * grid/list pages where many subscribers observe the viewport with the same\n * margin. Promoted from the inline singleton at\n * `multi-platform-hub/components/shared/video-bites-display.tsx:21-43`,\n * which is the only IO pattern in either repo today.\n *\n * Usage:\n * ```tsx\n * function MyCard() {\n *   const { ref, isNear } = useNearViewport('500px');\n *   return <div ref={ref}>{isNear ? <HeavyChild /> : <Placeholder />}</div>;\n * }\n * ```\n *\n * StrictMode safety: cleanup uses an identity check on the registered\n * callback so React's dev double-mount (mount → cleanup → re-mount) does\n * not drop the second mount's freshly-set subscription. The IO callback\n * also checks `subscribers.get(target)` before invoking so a fire that\n * races with unmount cannot crash on a torn-down component.\n *\n * The hook fires once — on first intersection it sets `isNear=true` and\n * unobserves the element. Callers that need re-observation should\n * unmount and remount (or fork the hook for two-way behavior).\n */\n\nimport { useEffect, useRef, useState, useCallback } from 'react';\n\n// Per-(rootMargin, threshold) IO map. Multiple call sites with different\n// margins/thresholds each get their own singleton observer.\nconst observers = new Map<string, IntersectionObserver>();\nconst subscribers = new WeakMap<Element, () => void>();\n\n// Slack for the explicit ratio check: an element sized to exactly meet the\n// threshold can report a ratio a hair under it (sub-pixel / float rounding), so\n// accept `ratio >= threshold - EPSILON`. With `threshold: 0` this is a no-op\n// (ratio is always ≥ 0 > -EPSILON) — the `isIntersecting` check alone gates.\nconst THRESHOLD_EPSILON = 0.01;\n\n/** Stable map key for an observer config. */\nfunction observerKey(rootMargin: string, threshold: number): string {\n  return `${rootMargin}|${threshold}`;\n}\n\nfunction getObserverFor(rootMargin: string, threshold: number): IntersectionObserver {\n  const key = observerKey(rootMargin, threshold);\n  const existing = observers.get(key);\n  if (existing) return existing;\n\n  const io = new IntersectionObserver(\n    (entries) => {\n      entries.forEach((entry) => {\n        // Enforce the threshold EXPLICITLY. `isIntersecting` is spec-defined as\n        // `intersectionRatio > 0` (any overlap), so the initial `observe()`\n        // firing can report `true` at, say, 20% visible even with\n        // `threshold: 0.5` — which would prematurely pass the gate. Require both\n        // an intersection AND the ratio to actually meet the threshold (minus a\n        // small epsilon for the float rounding where the ratio lands a hair\n        // under the exact value).\n        if (!entry.isIntersecting) return;\n        if (entry.intersectionRatio < threshold - THRESHOLD_EPSILON) return;\n        // Race-safe: re-read the callback at fire time. A late IO firing\n        // after cleanup must not invoke a stale callback.\n        const cb = subscribers.get(entry.target);\n        if (cb) {\n          cb();\n          io.unobserve(entry.target);\n          subscribers.delete(entry.target);\n        }\n      });\n    },\n    { rootMargin, threshold }\n  );\n  observers.set(key, io);\n  return io;\n}\n\n/**\n * Default near-viewport lookahead — the SSOT for \"mount media on approach\".\n * Shared by this hook's default AND the raw two-way IntersectionObservers in\n * `cards-strip.tsx` / `video-bites-strip.tsx` (which need mount+unmount, not\n * this hook's fire-once semantics, but must agree on the distance).\n */\nexport const NEAR_VIEWPORT_ROOT_MARGIN = '500px';\n\nexport interface UseNearViewportResult<T extends Element = HTMLElement> {\n  /** Ref to attach to the element you want to gate on visibility. */\n  ref: (node: T | null) => void;\n  /** Flips to `true` once the element enters within `rootMargin` of the viewport. Never flips back. */\n  isNear: boolean;\n}\n\n/**\n * @param rootMargin Margin around the viewport (CSS-style string).\n *                   '500px' = element starts mounting 500px before scroll-in.\n *                   '1000px' = a full viewport's worth of lookahead.\n *                   '0px' = strict on-screen detection.\n * @param threshold  Fraction of the element that must be visible before firing\n *                   (0 = any pixel — the default; 0.5 = at least half on-screen).\n */\nexport function useNearViewport<T extends Element = HTMLElement>(\n  rootMargin: string = NEAR_VIEWPORT_ROOT_MARGIN,\n  threshold: number = 0\n): UseNearViewportResult<T> {\n  const [isNear, setIsNear] = useState(false);\n  const elRef = useRef<T | null>(null);\n  const key = observerKey(rootMargin, threshold);\n\n  // Subscribe/unsubscribe on element change.\n  const ref = useCallback(\n    (node: T | null) => {\n      const prev = elRef.current;\n\n      // Unsubscribe previous, if any. Identity-check the callback so a\n      // StrictMode re-mount that has already re-registered keeps its sub.\n      if (prev) {\n        const stillOurs = subscribers.get(prev);\n        if (stillOurs) {\n          subscribers.delete(prev);\n          observers.get(key)?.unobserve(prev);\n        }\n      }\n\n      elRef.current = node;\n      if (!node) return;\n\n      const cb = () => setIsNear(true);\n      subscribers.set(node, cb);\n      getObserverFor(rootMargin, threshold).observe(node);\n    },\n    [key, rootMargin, threshold]\n  );\n\n  // Unsubscribe on unmount. Identity check guards the StrictMode race.\n  useEffect(() => {\n    return () => {\n      const el = elRef.current;\n      if (!el) return;\n      if (subscribers.get(el)) {\n        subscribers.delete(el);\n        observers.get(key)?.unobserve(el);\n      }\n    };\n  }, [key]);\n\n  return { ref, isNear };\n}\n","'use client'\n\n/**\n * Access Code Integration Hook\n *\n * React-side wrapper around the pure `access-code-client` utilities.\n * Lives in `hooks/` (client bundle) so the `createContext()` call in\n * `endpoints-runtime-context` doesn't get pulled into the server-safe\n * `utils/index` bundle.\n *\n * The pure standalone functions (`validateAccessCode`,\n * `consumeAccessCode`, `validateAndConsumeAccessCode`) remain importable\n * from `@flamingo-stack/openframe-frontend-core/utils` — they take the\n * endpoints object as an argument. This hook binds those endpoints from\n * `EndpointsRuntimeContext` so React callers don't have to plumb URLs.\n */\n\nimport React from 'react'\n\nimport { useRequiredEndpointsRuntime } from '../contexts/endpoints-runtime-context'\nimport {\n  validateAccessCode,\n  consumeAccessCode,\n  validateAndConsumeAccessCode,\n} from '../utils/access-code-client'\n\n/**\n * Resolves access-code endpoints from `EndpointsRuntimeContext` (throws\n * if no provider is mounted) and exposes loading-state-aware wrappers\n * around the standalone helpers in `utils/access-code-client`.\n *\n * @returns the following fields. The `validate` / `consume` /\n *   `validateAndConsume` functions and the returned object identity are\n *   NOT memoized — they're re-created each render. Wrap with\n *   `useCallback` / `useMemo` at the call site if downstream effect\n *   dep arrays depend on stable identities.\n *   - `validate(email, code)`: validates only.\n *   - `consume(email, code)`: consumes only.\n *   - `validateAndConsume(email, code)`: one-step validate-then-consume.\n *   - `isValidating: boolean`: a validate call is in flight.\n *   - `isConsuming: boolean`: a consume call is in flight.\n *   - `isProcessing: boolean`: convenience — `isValidating || isConsuming`.\n *     Use this for a single \"in-flight\" indicator on UI affordances that\n *     should disable during both phases.\n *\n * @example\n * const { validate, consume, isProcessing } = useAccessCodeIntegration();\n *\n * const handleRegistration = async (formData) => {\n *   const validation = await validate(formData.email, formData.accessCode);\n *   if (!validation.valid) {\n *     setError(validation.message);\n *     return;\n *   }\n *\n *   // Process registration...\n *   const registrationResult = await registerUser(formData);\n *\n *   if (registrationResult.success) {\n *     await consume(formData.email, formData.accessCode);\n *   }\n * };\n */\nexport function useAccessCodeIntegration() {\n  const runtime = useRequiredEndpointsRuntime()\n  const endpoints = runtime.accessCode\n  const [isValidating, setIsValidating] = React.useState(false)\n  const [isConsuming, setIsConsuming] = React.useState(false)\n\n  const validate = async (email: string, code: string) => {\n    setIsValidating(true)\n    try {\n      return await validateAccessCode(email, code, endpoints)\n    } finally {\n      setIsValidating(false)\n    }\n  }\n\n  const consume = async (email: string, code: string) => {\n    setIsConsuming(true)\n    try {\n      return await consumeAccessCode(email, code, endpoints)\n    } finally {\n      setIsConsuming(false)\n    }\n  }\n\n  const validateAndConsume = async (email: string, code: string) => {\n    setIsValidating(true)\n    setIsConsuming(true)\n    try {\n      return await validateAndConsumeAccessCode(email, code, endpoints)\n    } finally {\n      setIsValidating(false)\n      setIsConsuming(false)\n    }\n  }\n\n  return {\n    validate,\n    consume,\n    validateAndConsume,\n    isValidating,\n    isConsuming,\n    isProcessing: isValidating || isConsuming,\n  }\n}\n","/**\n * Access Code Client Utilities — pure standalone functions.\n *\n * Endpoint paths are NOT hardcoded — every function takes an\n * `endpoints` argument. The React-side wrapper that binds them from\n * `EndpointsRuntimeContext` lives separately at\n * `hooks/use-access-code-integration.ts` (`useAccessCodeIntegration`).\n *\n * Keep this file **free of React imports** — it lives in the\n * server-safe `utils/index` tsup bundle. Any module-top-level call\n * into `createContext()` (which the runtime context file does) would\n * be pulled into the server bundle and crash SSR with\n * `createContext is not a function`.\n */\n\nimport {\n  AccessCodeValidation,\n  AccessCodeValidationResponse,\n  AccessCodeConsumptionResponse\n} from '../types/access-code-cohorts';\n\n/** Endpoints required by the standalone client utilities. The\n *  `useAccessCodeIntegration` hook (in `hooks/`) resolves these from\n *  `EndpointsRuntimeContext.accessCode` automatically. */\nexport interface AccessCodeEndpoints {\n  validateUrl: string\n  consumeUrl: string\n}\n\n/**\n * Validate an access code for a given email\n *\n * @param email - User's email address\n * @param code - Access code to validate\n * @returns Promise with validation result\n *\n * @example\n * const result = await validateAccessCode('user@example.com', 'ABC123XY');\n * if (result.valid) {\n *   // Allow user to proceed with registration\n *   console.log(`Welcome to ${result.cohort_name}!`);\n * } else {\n *   // Show error message\n *   console.error(result.message);\n * }\n */\nexport async function validateAccessCode(\n  email: string,\n  code: string,\n  endpoints: AccessCodeEndpoints,\n): Promise<AccessCodeValidationResponse> {\n  try {\n    const response = await fetch(endpoints.validateUrl, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n      },\n      body: JSON.stringify({ email, code } as AccessCodeValidation),\n    });\n\n    if (!response.ok) {\n      const error = await response.json().catch(() => ({}));\n      throw new Error(error.error || 'Validation request failed');\n    }\n\n    return await response.json() as AccessCodeValidationResponse;\n  } catch (error) {\n    return {\n      valid: false,\n      message: error instanceof Error ? error.message : 'Validation failed',\n    };\n  }\n}\n\n/**\n * Consume an access code after successful registration\n *\n * Call this ONLY after the user has successfully completed registration.\n * This marks the code as used and prevents further usage.\n *\n * @param email - User's email address\n * @param code - Access code to consume\n * @returns Promise with consumption result\n *\n * @example\n * // After successful registration\n * const result = await consumeAccessCode('user@example.com', 'ABC123XY');\n * if (result.consumed) {\n *   console.log('Access code consumed successfully');\n * } else {\n *   console.warn('Failed to consume access code:', result.message);\n * }\n */\nexport async function consumeAccessCode(\n  email: string,\n  code: string,\n  endpoints: AccessCodeEndpoints,\n): Promise<AccessCodeConsumptionResponse> {\n  try {\n    const response = await fetch(endpoints.consumeUrl, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n      },\n      body: JSON.stringify({ email, code } as AccessCodeValidation),\n    });\n\n    if (!response.ok) {\n      const error = await response.json().catch(() => ({}));\n      throw new Error(error.error || 'Consumption request failed');\n    }\n\n    return await response.json() as AccessCodeConsumptionResponse;\n  } catch (error) {\n    return {\n      success: false,\n      consumed: false,\n      message: error instanceof Error ? error.message : 'Consumption failed',\n    };\n  }\n}\n\n/**\n * Complete access code flow: validate then consume\n *\n * This is a convenience function that validates an access code and,\n * if valid, immediately consumes it. Use this when you want to\n * validate and consume in one step during registration.\n *\n * @param email - User's email address\n * @param code - Access code to validate and consume\n * @returns Promise with validation and consumption results\n *\n * @example\n * const result = await validateAndConsumeAccessCode('user@example.com', 'ABC123XY');\n * if (result.valid && result.consumed) {\n *   // Registration successful\n *   console.log(`Welcome to ${result.cohort_name}!`);\n * } else {\n *   console.error(result.message);\n * }\n */\nexport async function validateAndConsumeAccessCode(\n  email: string,\n  code: string,\n  endpoints: AccessCodeEndpoints,\n): Promise<AccessCodeValidationResponse & { consumed?: boolean }> {\n  // First validate\n  const validation = await validateAccessCode(email, code, endpoints);\n\n  if (!validation.valid) {\n    return validation;\n  }\n\n  // If valid, consume the code\n  const consumption = await consumeAccessCode(email, code, endpoints);\n\n  return {\n    ...validation,\n    consumed: consumption.consumed,\n    message: consumption.consumed\n      ? `Access granted for ${validation.cohort_name}`\n      : consumption.message || validation.message,\n  };\n}\n\n// `useAccessCodeIntegration` (the React-side wrapper) lives in\n// `hooks/use-access-code-integration.ts`. It binds the endpoints from\n// `EndpointsRuntimeContext` so React callers don't have to plumb URLs.\n","'use client'\n\nimport { useMemo } from 'react'\n\nimport { useChatRuntime } from '../contexts/chat-runtime-context'\nimport { buildOgPlaceholderUrl } from '../utils/og-placeholder'\n\n/**\n * Resolve a branded og-placeholder image URL for a title, driven entirely by\n * the runtime `endpoints` (no injected builder).\n *\n * THE one og-placeholder hook. It reads the host's `endpoints` from\n * `ChatRuntime` and hands them to `buildOgPlaceholderUrl`, which resolves the\n * route base (explicit `ogPlaceholderUrl` → derived from `imageProxyUrlPrefix`\n * → same-origin `/api/og-placeholder`) and appends `?title=…`. Per-platform\n * brand colors are resolved SERVER-SIDE by the route — nothing is baked here.\n *\n * Replaces the old builder-injection `useOgPlaceholder(buildUrl, …)`: callers\n * no longer pass a URL builder. `useEntityCardPlaceholder` delegates here too,\n * so every surface shares one memo + one code path.\n */\nexport interface UseOgPlaceholderUrlArgs {\n  /** Text to display on the placeholder. */\n  title: string | undefined | null\n  /** Site name shown below the title (optional). */\n  siteName?: string\n  /** `'wide'` (1200×630 social-card; default) or `'square'` (1024×1024 — for\n   *  compact chat-inline slots so `object-cover` doesn't crop the title off). */\n  aspect?: 'wide' | 'square'\n  /** When `false`, returns `null` instead of a URL. */\n  enabled?: boolean\n}\n\nexport function useOgPlaceholderUrl({\n  title,\n  siteName = '',\n  aspect = 'wide',\n  enabled = true,\n}: UseOgPlaceholderUrlArgs): string | null {\n  const endpoints = useChatRuntime()?.endpoints\n\n  return useMemo(() => {\n    if (!enabled || !title) return null\n    return buildOgPlaceholderUrl(endpoints, title, { site: siteName || undefined, aspect })\n  }, [endpoints, title, siteName, aspect, enabled])\n}\n","/**\n * Branded OG-placeholder URL construction — the DEFAULT cover-image fallback\n * for entity cards (onboarding guides, blog/case-study/release/etc.) that have\n * no image.\n *\n * ALL of the logic lives here, in the lib. A consumer hands over its runtime\n * `endpoints` object and NOTHING else — base resolution AND the `?title=…`\n * concatenation happen inside `buildOgPlaceholderUrl`. For the entity-card +\n * onboarding-detail surfaces this is the single entry point — no card builds an\n * og-placeholder URL itself (that was the bug this replaced: each embedder\n * wired a per-surface callback that concatenated the URL, and a host that\n * forgot it rendered a blank slot with no request). (Other server-side OG paths\n * such as the hub's blog og:image generator construct their own URLs and are\n * out of this module's scope.)\n *\n * The base API URL is taken from the endpoints the host already configures:\n *   1. explicit `endpoints.ogPlaceholderUrl`\n *   2. derived from the sibling `endpoints.imageProxyUrlPrefix` (same API base)\n *   3. same-origin relative `/api/og-placeholder`\n * The base may already carry pre-existing query params; they're preserved and\n * `title` (+ dimensions) are layered on top. Per-platform brand colors are NOT\n * baked into this URL — the `/api/og-placeholder` route resolves them\n * server-side from the platform. Most hosts leave `ogPlaceholderUrl` unset and\n * let the base derive from `imageProxyUrlPrefix`.\n */\n\n/** The slice of `ChatRuntime.endpoints` this module needs. */\nexport interface OgPlaceholderEndpoints {\n  /** Explicit base URL for the og-placeholder route. May already carry query\n   *  params — they're preserved. Per-platform colors are NOT baked here; the\n   *  route resolves them server-side from the platform. */\n  ogPlaceholderUrl?: string\n  /** Sibling image route under the SAME API base. When `ogPlaceholderUrl` is\n   *  unset, the base is derived from this by swapping the trailing\n   *  `/image-proxy` segment for `/og-placeholder` — so a host that already\n   *  proxies images gets the placeholder for free, with zero extra wiring. */\n  imageProxyUrlPrefix?: string\n}\n\nexport interface BuildOgPlaceholderOptions {\n  /** Site name shown under the title. Skipped when empty. */\n  site?: string\n  /** `'wide'` (1200×630, the route default — no `w`/`h` emitted) or\n   *  `'square'` (1024×1024, for compact 56×56 chat-inline slots). */\n  aspect?: 'wide' | 'square'\n  /** Explicit pixel overrides (win over `aspect`). */\n  width?: number\n  height?: number\n}\n\n/** Same-origin default — for hosts that serve the route themselves (the hub).\n *  Cross-origin embedders override via `ogPlaceholderUrl` or inherit it from\n *  `imageProxyUrlPrefix`. */\nconst DEFAULT_OG_PLACEHOLDER_PATH = '/api/og-placeholder'\n\n/** Resolve the og-placeholder route base from the host's endpoints.\n *  Internal — callers go through `buildOgPlaceholderUrl(endpoints, …)`. */\nfunction resolveOgPlaceholderBase(endpoints?: OgPlaceholderEndpoints | null): string {\n  if (endpoints?.ogPlaceholderUrl) return endpoints.ogPlaceholderUrl\n  const imageProxy = endpoints?.imageProxyUrlPrefix\n  if (imageProxy) {\n    // `/image-proxy` and `/og-placeholder` are sibling API routes under one\n    // base. Anchor to a path-segment boundary so we only rewrite the route\n    // name, never an incidental substring.\n    const derived = imageProxy.replace(/\\/image-proxy(?=$|[?/])/, '/og-placeholder')\n    if (derived !== imageProxy) return derived\n  }\n  return DEFAULT_OG_PLACEHOLDER_PATH\n}\n\n/**\n * Build the branded og-placeholder image URL from the host's `endpoints` + a\n * title. This is the single entry point for entity-card + onboarding-detail\n * cover fallbacks: it resolves the route base from `endpoints` AND concatenates\n * `title` (+ dimensions), so those consumers never construct a URL themselves.\n *\n * Pure string construction — SSR- and browser-safe. Always returns a usable\n * URL (relative default at worst), so a missing/unknown image degrades\n * gracefully via the `<img onError>` recovery in the card components.\n */\nexport function buildOgPlaceholderUrl(\n  endpoints: OgPlaceholderEndpoints | null | undefined,\n  title: string,\n  options: BuildOgPlaceholderOptions = {},\n): string {\n  const base = resolveOgPlaceholderBase(endpoints)\n  const qIndex = base.indexOf('?')\n  const path = qIndex === -1 ? base : base.slice(0, qIndex)\n  const params = new URLSearchParams(qIndex === -1 ? '' : base.slice(qIndex + 1))\n\n  params.set('title', title)\n  if (options.site) params.set('site', options.site)\n\n  // Square aspect → request a 1024×1024 image so `object-cover` doesn't crop\n  // the title off in compact slots. Wide leaves dimensions to the route\n  // default (1200×630). Explicit width/height always win.\n  const width = options.width ?? (options.aspect === 'square' ? 1024 : undefined)\n  const height = options.height ?? (options.aspect === 'square' ? 1024 : undefined)\n  // `Number.isFinite` does not coerce, so it already rejects `undefined` — no\n  // separate `typeof === 'number'` guard needed.\n  if (Number.isFinite(width)) params.set('w', String(width))\n  if (Number.isFinite(height)) params.set('h', String(height))\n\n  return `${path}?${params.toString()}`\n}\n","'use client'\n\nimport { useEffect, useState } from 'react'\nimport { useEndpointsRuntime } from '../contexts/endpoints-runtime-context'\nimport { toClaudeMirrorPath } from '../utils/embed-url-converters'\n\nexport type ClaudeMirrorStatus =\n  /** No mirror candidate at all (no proxy configured, or a non-mirrorable url). */\n  | 'absent'\n  /** Probing the host for a self-hosted mirror (or minting one on first view). */\n  | 'probing'\n  /** A mirror exists and `src` points at it. */\n  | 'found'\n\nexport interface ClaudeMirrorState {\n  /** The mirror src when found (with a cache-bust once a re-publish is\n   *  detected), else `null`. */\n  src: string | null\n  status: ClaudeMirrorStatus\n}\n\n/**\n * Ask the host's storage-view proxy whether the cached mirror is still the\n * artifact's CURRENT version, re-minting it silently if a re-publish moved\n * it. Returns a cache-bust token (the new version) when the mirror changed,\n * else null. Best-effort: any failure just leaves the shown mirror in place.\n */\nasync function revalidateMirror(mirrorPath: string): Promise<string | null> {\n  try {\n    const res = await fetch(`${mirrorPath}?revalidate=1`)\n    if (!res.ok) return null\n    const body = (await res.json()) as { updated?: boolean; version?: string | null }\n    return body?.updated ? String(body.version ?? Date.now()) : null\n  } catch {\n    return null\n  }\n}\n\n/**\n * The host's SELF-HOSTED MIRROR src for a claude artifact url, plus a\n * `status` so the viewer can show a loading state while it resolves — THE\n * hook behind `ClaudeEmbed`'s transparent mirror leg, extracted so the\n * component stays presentational.\n *\n * Fully under the hood, three moves:\n *   1. Derive the candidate mirror path from the url's artifact id under the\n *      host's configured storage-view proxy base\n *      (`EndpointsRuntime.storageViewBaseUrl`) and probe it with a 1-byte\n *      ranged GET. On a cold first view that probe IS the server-side\n *      self-heal (~1-2s), which is exactly why `status: 'probing'` exists —\n *      the viewer shows a spinner instead of the empty \"no embeddable view\"\n *      flash. Exists → `found`; missing / no proxy / blip → `absent`, and the\n *      caller's claude.ai fallback stands (the pre-mirror behavior).\n *   2. Once found, fire ONE background revalidation. If the artifact was\n *      re-published since the mirror was cached, the server re-mints it and\n *      reports the new version; the hook bumps a cache-bust on `src` so the\n *      iframe silently reloads the fresh bytes. Unchanged → nothing happens.\n *\n * A host that mounts no EndpointsRuntime provider (or omits the entry) skips\n * the mirror leg entirely (`absent`).\n */\nexport function useClaudeMirrorSrc(url: string): ClaudeMirrorState {\n  const endpoints = useEndpointsRuntime()\n  const mirrorPath = toClaudeMirrorPath(url, endpoints?.storageViewBaseUrl)\n  const [state, setState] = useState<ClaudeMirrorState>(() => ({\n    src: null,\n    status: mirrorPath ? 'probing' : 'absent',\n  }))\n\n  useEffect(() => {\n    if (!mirrorPath) {\n      setState({ src: null, status: 'absent' })\n      return\n    }\n    setState({ src: null, status: 'probing' })\n    let cancelled = false\n    fetch(mirrorPath, { headers: { Range: 'bytes=0-0' } })\n      .then((res) => {\n        if (cancelled) return\n        if (!(res.ok || res.status === 206)) {\n          setState({ src: null, status: 'absent' })\n          return\n        }\n        setState({ src: mirrorPath, status: 'found' })\n        // Background freshness check — reload in place only if it changed.\n        revalidateMirror(mirrorPath).then((bust) => {\n          if (!cancelled && bust) {\n            setState({ src: `${mirrorPath}?v=${encodeURIComponent(bust)}`, status: 'found' })\n          }\n        })\n      })\n      .catch(() => {\n        // No proxy on this host / network blip — the caller's claude.ai\n        // fallback stands, same as before mirrors existed.\n        if (!cancelled) setState({ src: null, status: 'absent' })\n      })\n    return () => {\n      cancelled = true\n    }\n  }, [mirrorPath])\n\n  return state\n}\n","/**\n * Shared utilities for converting external service URLs to embeddable formats.\n * Used by lib's embed components and the hub's admin document editor.\n */\n\nexport function toGoogleSheetsEmbedUrl(url: string): string {\n  if (url.includes('/htmlembed')) return url\n\n  const match = url.match(/\\/spreadsheets\\/d\\/([a-zA-Z0-9-_]+)/)\n  if (!match) return url\n\n  const gidMatch = url.match(/[#?&]gid=(\\d+)/)\n  const gid = gidMatch ? gidMatch[1] : '0'\n\n  return `https://docs.google.com/spreadsheets/d/${match[1]}/htmlembed?widget=true&chrome=false&headers=false&gid=${gid}`\n}\n\nexport function toGoogleSheetsOriginalUrl(url: string): string {\n  const match = url.match(/\\/spreadsheets\\/d\\/([a-zA-Z0-9-_]+)/)\n  if (!match) return url\n\n  const gidMatch = url.match(/[#?&]gid=(\\d+)/)\n  const gid = gidMatch ? gidMatch[1] : '0'\n\n  return `https://docs.google.com/spreadsheets/d/${match[1]}/edit#gid=${gid}`\n}\n\n/**\n * Convert a Figma URL to an embeddable URL.\n * Slides/deck URLs map to `deck` (present) by default; `slidesView: 'browse'` switches to `slides` (browse).\n */\nexport function toFigmaEmbedUrl(\n  url: string,\n  opts?: { slidesView?: 'present' | 'browse' }\n): string {\n  if (url.includes('embed.figma.com')) return url\n  if (url.includes('figma.com/embed')) return url\n\n  const match = url.match(\n    /figma\\.com\\/(design|file|proto|board|slides|deck)\\/([a-zA-Z0-9]+)(?:\\/([^?]*))?(\\?.*)?$/\n  )\n\n  if (match) {\n    const [, urlType, fileKey, titleSlug, queryString] = match\n    const isSlides = urlType === 'slides' || urlType === 'deck'\n    const embedType =\n      urlType === 'proto' ? 'proto'\n      : isSlides ? (opts?.slidesView === 'browse' ? 'slides' : 'deck')\n      : 'design'\n    const pathSuffix = titleSlug ? `/${titleSlug}` : ''\n\n    const params = new URLSearchParams(queryString?.replace(/^\\?/, '') || '')\n    if (!params.has('embed-host')) {\n      params.set('embed-host', 'flamingo')\n    }\n    const clientId = process.env.NEXT_PUBLIC_FIGMA_CLIENT_ID\n    if (clientId && !params.has('client-id')) {\n      params.set('client-id', clientId)\n    }\n\n    return `https://embed.figma.com/${embedType}/${fileKey}${pathSuffix}?${params.toString()}`\n  }\n\n  return `https://www.figma.com/embed?embed-host=flamingo&url=${encodeURIComponent(url)}`\n}\n\nexport function isFigmaSlidesUrl(url: string): boolean {\n  if (!url) return false\n  return /(?:www\\.|embed\\.)?figma\\.com\\/(?:slides|deck)\\/[a-zA-Z0-9]+/.test(url)\n}\n\nexport function toFigmaOriginalUrl(url: string): string {\n  if (url.includes('embed.figma.com')) {\n    return url.replace('embed.figma.com', 'www.figma.com').replace(/\\?.*$/, '')\n  }\n  return url.replace(/\\?.*$/, '')\n}\n\n/**\n * The self-hosted MIRROR url for a claude artifact url — the whole\n * mirror mechanism is UNDER THE HOOD: the viewer detects a claude url,\n * DERIVES the mirror location from the artifact's own id under the\n * host's configured storage-view proxy base, and frames the downloaded\n * copy from there. No per-link data, no props, no shortcode changes —\n * 100% transparent to every consumer.\n *\n * `storageViewBaseUrl` comes from `EndpointsRuntime` — the SAME\n * host-overridable proxy-path mechanism every other lib→API call uses\n * (a reverse-proxied embedder remaps it; a host without the proxy omits\n * it and gets `null` here, i.e. no mirror leg at all).\n *\n * `null` when the base is absent or the url carries no artifact id. The\n * derived url is the configured base + a validated uuid — safe as a\n * frame source without further checks. KEEP THE OBJECT NAMING IN SYNC\n * with the hub's `lib/data/design-brief-mirror.cjs` (bucket +\n * `<uuid>.html`) — that module owns the ingest/audit half.\n */\nexport function toClaudeMirrorPath(\n  url: string | null | undefined,\n  storageViewBaseUrl: string | null | undefined,\n): string | null {\n  if (!url || !storageViewBaseUrl) return null\n  try {\n    const u = new URL(url)\n    const host = u.hostname.toLowerCase()\n    const id =\n      (host === 'claude.ai' &&\n        /^\\/(?:code\\/artifact|public\\/artifacts)\\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[/?#]|$)/i.exec(\n          u.pathname,\n        )?.[1]) ||\n      (host === 'claude.site' &&\n        /^\\/artifacts\\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[/?#]|$)/i.exec(\n          u.pathname,\n        )?.[1]) ||\n      null\n    return id\n      ? `${storageViewBaseUrl.replace(/\\/$/, '')}/design-briefs/${id.toLowerCase()}.html`\n      : null\n  } catch {\n    return null\n  }\n}\n\n/**\n * The EMBED url for a published Claude artifact, or `null` when the link is not\n * one we can frame — the Claude counterpart of `toFigmaEmbedUrl`.\n *\n * claude.ai serves `frame-ancestors 'self'` on the artifact page itself, but a\n * PUBLISHED artifact also has an `/embed` route whose `frame-ancestors` carries\n * the domains its author allow-listed under \"Get embed code → Allowed domains\".\n * That is Anthropic's supported way to put an artifact on another site, so that\n * is what the iframe points at.\n *\n * Returns `null` for everything else, including a Claude CODE url\n * (`claude.ai/code/artifact/…`), whose Share dialog exposes no Embed settings\n * and whose `/embed` path answers `frame-ancestors 'self'` (verified against\n * real artifacts, 2026-08). A trailing `/embed` is accepted because that is the\n * url \"Get embed code\" hands the author.\n */\nexport function toClaudeEmbedUrl(url: string | null | undefined): string | null {\n  if (!url) return null\n  try {\n    const u = new URL(url)\n    const host = u.hostname.toLowerCase()\n    // `claude.site/artifacts/<id>` 308-redirects to the claude.ai public url.\n    const publicId =\n      (host === 'claude.ai' && /^\\/public\\/artifacts\\/([\\w-]+)(?:\\/embed)?\\/?$/.exec(u.pathname)?.[1]) ||\n      (host === 'claude.site' && /^\\/artifacts\\/([\\w-]+)(?:\\/embed)?\\/?$/.exec(u.pathname)?.[1]) ||\n      null\n    return publicId ? `https://claude.ai/public/artifacts/${publicId}/embed` : null\n  } catch {\n    return null\n  }\n}\n","'use client'\n\nimport { useEffect } from 'react'\nimport { scrollElementIntoView } from '../utils/scroll-into-view'\nimport { getHashTargetElement, normalizeHashFragment } from '../utils/same-page-hash-nav'\n\n/** ~1s at 60fps — long enough to outlast Radix accordion expand + SWR\n *  mount, short enough that a missed anchor doesn't hang the page. */\nconst MAX_POLL_FRAMES = 60\n\nexport interface UseScrollToHashOptions {\n  /** Pixels to subtract for sticky chrome. */\n  headerOffset?: number\n}\n\n/**\n * Scroll the page to `window.location.hash` once `readyDep` resolves\n * to a truthy value. Polls via rAF for ~1s so lazy-mounted rows (Radix\n * accordion, SWR fetch) have time to render. Re-runs on `readyDep`\n * reference change AND on `hashchange` (browser back/forward + the\n * synthetic event `navigateSamePageHash` dispatches).\n *\n * Skipped when `readyDep == null || readyDep === false`. Default\n * `true` makes the hook run on mount for pages whose target is in the\n * initial SSR render.\n */\nexport function useScrollToHash(\n  readyDep: unknown = true,\n  options?: UseScrollToHashOptions,\n): void {\n  const headerOffset = options?.headerOffset ?? 0\n  useEffect(() => {\n    if (typeof window === 'undefined') return\n    if (readyDep === null || readyDep === false) return\n    let rafId: number | null = null\n    const cancelPoll = () => {\n      if (rafId !== null) {\n        cancelAnimationFrame(rafId)\n        rafId = null\n      }\n    }\n    const tryScrollToHash = () => {\n      // `normalizeHashFragment` heals a malformed multi-fragment hash\n      // so `getElementById` resolves on deep-link entries that bypass\n      // `navigateSamePageHash`'s own normalize.\n      const hash = normalizeHashFragment(window.location.hash).slice(1)\n      if (!hash) return\n      // Cancel any in-flight poll from a prior invocation so two\n      // concurrent ticks can't both call scrollElementIntoView.\n      cancelPoll()\n      let frames = 0\n      const tick = () => {\n        // Raw-then-decoded lookup — encoded fragments from href\n        // composers must match the unencoded ids rows render.\n        const el = getHashTargetElement(hash)\n        if (el) {\n          rafId = null\n          scrollElementIntoView(el, { headerOffset })\n          return\n        }\n        if (frames++ < MAX_POLL_FRAMES) {\n          rafId = requestAnimationFrame(tick)\n        } else {\n          rafId = null\n        }\n      }\n      tick()\n    }\n    tryScrollToHash()\n    window.addEventListener('hashchange', tryScrollToHash)\n    return () => {\n      window.removeEventListener('hashchange', tryScrollToHash)\n      cancelPoll()\n    }\n  }, [readyDep, headerOffset])\n}\n","/**\n * `scrollElementIntoView` — canonical \"scroll an element to the top of the\n * viewport, account for sticky chrome, survive layout shifts\" helper.\n *\n * One shared implementation so every caller (the ticket drawer expand, the\n * hub's `useUnifiedNav` / `use-nav-link` hash scroll, doc-tree, delivery\n * `?focus=`, sticky-section-nav, …) inherits the SAME cancellation-proof\n * motion.\n *\n * WHY A SELF-DRIVEN rAF TWEEN INSTEAD OF `window.scrollTo({behavior:'smooth'})`:\n * the native smooth scroll is CANCELLABLE, and in real pages it gets cancelled\n * constantly:\n *\n *   - Browser SCROLL ANCHORING: when content is inserted/removed above or\n *     around the target (a collapsible drawer expanding, an async image\n *     loading, a list re-rendering) the browser issues a synchronous scrollTop\n *     correction to keep the anchored element stable. Per CSSOM-View \"perform a\n *     scroll\" step 1 (\"abort any ongoing smooth scroll\"), that correction\n *     ABORTS an in-flight native smooth scroll — so it lands as an instant jump.\n *     Anchoring is suppressed when the scroll offset is 0, which is exactly why\n *     a native smooth scroll appears to work the FIRST time (page at top) and\n *     jumps on every repeat (page already scrolled). This was a multi-day\n *     \"smooth only works once\" bug on the /tickets drawer.\n *   - A second programmatic scroll on the same frame, or a `focus()` without\n *     `{preventScroll:true}`, cancels it the same way.\n *\n * A tween that re-asserts the position with INSTANT writes every frame is\n * immune: there is no \"ongoing native smooth scroll\" for anchoring/focus to\n * abort, and any correction that lands between our frames is overwritten on the\n * next frame. We also RECOMPUTE the target each frame, so an element whose\n * final position is still settling (drawer still expanding, images loading)\n * is tracked to its resting place instead of animating to a stale pixel.\n *\n * Honors `prefers-reduced-motion` (jumps instantly) and cancels on genuine user\n * scroll intent (wheel / touch) so we never fight the user.\n *\n * WINDOW *OR* A SCROLLABLE ANCESTOR: the helper is not hard-wired to the window\n * scroller. It walks up from the target to the nearest ancestor that is an\n * actual scroll container (`overflow-y: auto | scroll | overlay` AND\n * `scrollHeight > clientHeight`) and drives THAT element; only when none exists\n * does it fall back to `window`. This is what makes it work inside app shells\n * that put page content in a fixed-height `<main class=\"overflow-y-auto\">`\n * (e.g. OpenFrame's `AppLayout`) where the document/window never scrolls — the\n * old window-only version was a silent no-op there. Note `overflow: clip` /\n * `hidden` are deliberately NOT treated as scroll containers, so a list wrapper\n * that uses `overflow-clip` only to round its corners still bubbles the scroll\n * up to the real container (matches the `<HelpCenterCard>` list intent).\n */\n\nexport interface ScrollElementIntoViewOptions {\n  /** Pixels to subtract from the target element's `top` so it lands BELOW\n   *  sticky chrome. Defaults to 0. Pass `96` for the standard hub header. */\n  headerOffset?: number\n  /** `'smooth'` (default) runs the self-driven tween; `'instant'` / `'auto'`\n   *  jump in one synchronous write (deep-link land, programmatic focus moves). */\n  behavior?: ScrollBehavior\n  /** Optional adjustment applied to the computed pixel target each frame. The\n   *  callback receives the \"raw\" Y (`element.top + scrollY - headerOffset`) and\n   *  returns the FINAL target. Use when the caller knows about a layout shift\n   *  (e.g. a sibling drawer collapsing) the geometry can't yet reflect. */\n  adjustTargetY?: (rawTargetY: number) => number\n  /** Tween duration in ms (smooth only). Default 320. */\n  durationMs?: number\n}\n\n/** Module-level handle to the in-flight tween so a new call (or a user\n *  gesture) cancels the previous one — only ever one page-scroll animation at\n *  a time. */\nlet activeRaf = 0\nlet teardownActive: (() => void) | null = null\n\nfunction cancelActiveScroll(): void {\n  if (activeRaf) {\n    cancelAnimationFrame(activeRaf)\n    activeRaf = 0\n  }\n  if (teardownActive) {\n    teardownActive()\n    teardownActive = null\n  }\n}\n\nconst easeOutCubic = (t: number): number => 1 - Math.pow(1 - t, 3)\n\n/** Nearest ancestor that is a *real* scroll container, or `null` when the\n *  window/document is the scroller. Only `auto | scroll | overlay` count —\n *  `clip` / `hidden` are intentionally excluded (a wrapper using `overflow-clip`\n *  purely to round corners must let the scroll bubble to the page). */\nfunction getScrollableAncestor(el: HTMLElement): HTMLElement | null {\n  for (let node = el.parentElement; node; node = node.parentElement) {\n    const overflowY = getComputedStyle(node).overflowY\n    if (\n      (overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay') &&\n      node.scrollHeight > node.clientHeight\n    ) {\n      return node\n    }\n  }\n  return null\n}\n\n/**\n * Scroll the page so `target` lands at the top of the viewport (below sticky\n * chrome via `headerOffset`). SSR-safe; `null`/`undefined` target is a no-op so\n * callers can pass refs without defensive branching.\n */\nexport function scrollElementIntoView(\n  target: HTMLElement | null | undefined,\n  options: ScrollElementIntoViewOptions = {},\n): void {\n  if (typeof window === 'undefined' || !target) return\n  const { headerOffset = 0, behavior = 'smooth', adjustTargetY, durationMs = 320 } = options\n\n  // Pick the scroller ONCE: a fixed-height `<main overflow-y-auto>` shell scrolls\n  // the element, a plain document scrolls the window. The choice can't change\n  // mid-tween, so resolve it up front and route every read/write through it.\n  const container = getScrollableAncestor(target)\n  const readCurrent = (): number => (container ? container.scrollTop : window.scrollY)\n  const writeTo = (y: number): void => {\n    if (container) container.scrollTop = y\n    else window.scrollTo(0, y)\n  }\n\n  // Target is recomputed every frame: the row's absolute position can move as\n  // the page reflows (a sibling drawer collapsing) and the reachable max grows\n  // as the just-opened drawer expands. Clamp to the LIVE max each frame.\n  const computeTarget = (): number => {\n    const raw = container\n      ? container.scrollTop +\n        (target.getBoundingClientRect().top - container.getBoundingClientRect().top) -\n        headerOffset\n      : target.getBoundingClientRect().top + window.scrollY - headerOffset\n    const adjusted = adjustTargetY ? adjustTargetY(raw) : raw\n    const maxScroll = container\n      ? Math.max(0, container.scrollHeight - container.clientHeight)\n      : Math.max(0, document.documentElement.scrollHeight - window.innerHeight)\n    return Math.min(Math.max(0, adjusted), maxScroll)\n  }\n\n  // Any prior animation loses — one page scroll at a time.\n  cancelActiveScroll()\n\n  const prefersReduced =\n    typeof window.matchMedia === 'function' &&\n    window.matchMedia('(prefers-reduced-motion: reduce)').matches\n\n  // Instant paths: a single synchronous write. No tween, no anchoring race.\n  if (behavior === 'instant' || behavior === 'auto' || prefersReduced) {\n    writeTo(computeTarget())\n    return\n  }\n\n  // Smooth: self-driven tween with instant per-frame writes (anchoring-proof).\n  let startY: number | null = null\n  let startTime = 0\n\n  // Bail the moment the user takes over with a real scroll gesture — we must\n  // never fight them. (Not keydown: the ticket composer auto-focuses on open,\n  // and typing there should not abort the scroll.)\n  const onUserGesture = () => cancelActiveScroll()\n  window.addEventListener('wheel', onUserGesture, { passive: true })\n  window.addEventListener('touchmove', onUserGesture, { passive: true })\n  teardownActive = () => {\n    window.removeEventListener('wheel', onUserGesture)\n    window.removeEventListener('touchmove', onUserGesture)\n  }\n\n  const step = (now: number) => {\n    if (startY === null) {\n      startY = readCurrent()\n      startTime = now\n    }\n    const targetY = computeTarget()\n    const t = Math.min(1, (now - startTime) / durationMs)\n    const y = startY + (targetY - startY) * easeOutCubic(t)\n    writeTo(y)\n    if (t < 1) {\n      activeRaf = requestAnimationFrame(step)\n    } else {\n      // Final exact write in case easing left a sub-pixel gap, then teardown.\n      writeTo(computeTarget())\n      activeRaf = 0\n      if (teardownActive) {\n        teardownActive()\n        teardownActive = null\n      }\n    }\n  }\n  activeRaf = requestAnimationFrame(step)\n}\n","/**\n * Fenced-code tracker — THE definition of \"am I inside a code fence\".\n * Server-safe: no React, no DOM.\n *\n * Lives in its own module because it is a GENERAL CommonMark fence machine\n * with two unrelated consumers: the heading scanner\n * (`utils/markdown-heading-id`) and the streaming block splitter\n * (`components/ui/markdown/streaming`). It used to live inside\n * `markdown-heading-id`, which meant a streaming renderer imported its fence\n * state machine from a module named after heading slugs. `markdown-heading-id`\n * re-exports these names so the public `utils` surface is unchanged.\n */\n\n/**\n * CommonMark fenced-code opener/closer. Fence indent is capped at 3 spaces\n * (4+ is an indented code block, whose backticks are literal content), and the\n * run length is captured so a closer can be required to be at least as long as\n * its opener. Matched on the RAW line — `trimStart()` would let a fence inside\n * 4-space-indented code toggle the state.\n *\n * `\\r`-TOLERANT BY CONSTRUCTION. The previous spelling ended `(.*)$`, and `.`\n * excludes `\\r` while `$` (no `m` flag) anchors at end of input — so on a CRLF\n * document, split on `\\n`, NO line ever matched and the tracker was completely\n * fence-blind: `['```js\\r','const a = 1\\r','```\\r','after\\r']` classified as\n * four `text` lines where the LF spelling gives `open/inside/close/text`. The\n * sanitizer's closer haystack then never blanked the fenced region, so a\n * `</textarea>` written as a CRLF code sample satisfied `hasLaterCloser` and a\n * prose `<textarea>` above it stayed LIVE (reproduced in the DOM; the LF twin\n * of the same fixture escapes correctly). The `\\r` is absorbed by `\\r?$` rather\n * than normalized out of the text, because the sanitizer's mask is\n * LENGTH-PRESERVING and every offset in it is an index into the original\n * string — stripping `\\r` would shift them all.\n *\n * THE ONLY COPY. This constant plus its open/close state machine used to be\n * duplicated verbatim in `components/ui/markdown/streaming.ts`, kept in sync\n * by a code comment — which is exactly how the `~~~`-blind extractor drifted\n * in the first place. Both consumers now instantiate `createFenceTracker()`.\n */\nconst FENCE_RE = /^ {0,3}(`{3,}|~{3,})([^\\n]*?)\\r?$/\n\n/**\n * CommonMark's BLANK LINE — spaces and tabs only. The single definition, shared\n * by every consumer of this module (the sanitizer's mask passes, the heading\n * scanner, the HTML-block range walk).\n *\n * NOT `String.prototype.trim()`. JS `trim()` strips the full Unicode\n * White_Space set — U+00A0 NBSP, U+000B VT, U+000C FF, U+2028/9, U+FEFF BOM —\n * none of which CommonMark (or remark) treats as blank. Every place that meant\n * \"is this a CommonMark blank line?\" and asked `trim() === ''` therefore\n * TERMINATED a construct one line early while remark kept it open. Verified\n * end-to-end: `<div>\\n \\n\\`<textarea>\\`\\n\\nrest\\n` left a LIVE `<textarea>`\n * in the DOM (`escapeUnknownHtmlTags` returned the input byte-identical),\n * because the tracked HTML-block range ended at the NBSP filler line and the\n * inline-code shelter below it stopped being inside a tracked block. Same for\n * VT / FF / BOM, for `<pre>` / `<details>` wrappers, and for a live third-party\n * `<iframe>`.\n *\n * `\\r` IS accepted: a CRLF document split on `\\n` spells its blank lines\n * `\"\\r\"`, and the mask is length-preserving so stripping `\\r` from the source\n * is not an option (see `FENCE_RE`'s `\\r?$`).\n */\nconst BLANK_LINE_RE = /^[ \\t\\r]*$/\n\n/** True when `line` is a CommonMark blank line (spaces/tabs only, `\\r`-tolerant). */\nexport function isBlankLine(line: string): boolean {\n  return BLANK_LINE_RE.test(line)\n}\n\n/**\n * What one source line is, relative to fenced code:\n *  - `open`  — this line opened a fence\n *  - `close` — this line closed the open fence\n *  - `inside`— this line is fence content (including a would-be delimiter that\n *              does NOT close the open fence: wrong marker char, too short, or\n *              carrying an info string)\n *  - `text`  — this line is ordinary markdown outside any fence\n *\n * NOTE: no consumer discriminates the three non-`text` roles today — both the\n * heading scanner and the streaming splitter only test `!== 'text'`, and the\n * closer-haystack mask below only tests `=== 'open'` / `=== 'close'`. The\n * four-value union is kept because the state machine already computes the\n * distinction for free and a future consumer (e.g. fence-aware syntax\n * post-processing) would otherwise have to re-derive it.\n */\nexport type FenceLineRole = 'open' | 'close' | 'inside' | 'text'\n\nexport interface FenceTracker {\n  /** Advance across one raw source line (mutates) and classify it. */\n  push(line: string): FenceLineRole\n  /** Marker CHARACTER of the currently open fence (`` ` `` / `~`), or null. */\n  openChar(): string | null\n  /** Run length of the currently open fence (a closer must be ≥ this). */\n  openLength(): number\n}\n\n/** A fresh fenced-code state machine. Server-safe: no React, no DOM. */\nexport function createFenceTracker(): FenceTracker {\n  let fenceChar: string | null = null\n  let fenceLength = 0\n  return {\n    push(line: string): FenceLineRole {\n      const fence = FENCE_RE.exec(line)\n      if (fence) {\n        const run = fence[1]\n        if (fenceChar === null) {\n          fenceChar = run[0]\n          fenceLength = run.length\n          return 'open'\n        }\n        // A closer must use the SAME marker char, be at least as long as the\n        // opener, and (per CommonMark) carry no info string. \"No info string\"\n        // means spaces/tabs only — `trim()` would accept an NBSP tail and close\n        // a fence CommonMark keeps open, i.e. blank LESS of the haystack\n        // (fail-OPEN). Hence `isBlankLine`, not `trim()`.\n        if (run[0] === fenceChar && run.length >= fenceLength && isBlankLine(fence[2])) {\n          fenceChar = null\n          fenceLength = 0\n          return 'close'\n        }\n        return 'inside'\n      }\n      return fenceChar === null ? 'text' : 'inside'\n    },\n    openChar: () => fenceChar,\n    openLength: () => fenceLength,\n  }\n}\n","/**\n * Markdown heading-id slug SSOT (server-safe: no React, no DOM).\n *\n * This algorithm previously existed in THREE byte-identical copies:\n *   - `components/ui/simple-markdown-renderer.tsx` (generateHeadingId)\n *   - `components/ui/rich-markdown-renderer.tsx` (generateHeadingId)\n *   - `utils/markdown-section-extractor.ts` (extractSections)\n *\n * The extractor is the PRODUCER of `sectionIds` and the renderers are the\n * CONSUMERS — if the two ever drift, deep-link anchors and scroll-spy\n * targets silently diverge. All three now call these helpers; the parity\n * test `components/ui/__tests__/markdown-parity.test.tsx` asserts\n * extractor-vs-renderer ID agreement over the fixture corpus.\n */\n\n/** Emoji ranges stripped from heading text before slugification. */\nexport const HEADING_EMOJI_RE =\n  /[\\u{1F300}-\\u{1F9FF}]|[\\u{2600}-\\u{26FF}]|[\\u{2700}-\\u{27BF}]/gu\n\n/** Remove emoji characters and trim. */\nexport function stripHeadingEmojis(text: string): string {\n  return text.replace(HEADING_EMOJI_RE, '').trim()\n}\n\n/**\n * Core slug chain WITHOUT the emoji strip (the extractor exposes emoji\n * stripping as an option, so the two steps are kept separable):\n * lowercase → drop non-word/space/hyphen chars → spaces to hyphens →\n * trim leading/trailing hyphens. May return `''` for symbol-only input —\n * callers apply their own fallback (`section-N`).\n */\nexport function slugifyHeadingBase(text: string): string {\n  return text\n    .trim()\n    .toLowerCase()\n    .replace(/[^\\w\\s-]/g, '')\n    .replace(/\\s+/g, '-')\n    .replace(/^-+|-+$/g, '')\n}\n\n/** The full default chain used by the renderers: emoji strip + slugify. */\nexport function slugifyHeadingText(text: string): string {\n  return slugifyHeadingBase(stripHeadingEmojis(text))\n}\n\n// ---------------------------------------------------------------------------\n// Heading SCANNER + DEDUPER (the other half of the producer/consumer contract)\n// ---------------------------------------------------------------------------\n/**\n * Sharing the slug chain alone was not enough. The PRODUCER\n * (`utils/markdown-section-extractor`) and the CONSUMER\n * (`components/ui/markdown/heading-ids`) each carried their own copy of\n * \"which lines are headings\" and \"how do duplicates get suffixed\", and the\n * copies drifted:\n *   - the extractor toggled its code-block state on a bare\n *     `line.startsWith('```')`, so a `~~~`-fenced (or wider-backtick, or\n *     indented) block containing `## Setup` produced a SECTION from the\n *     extractor and NO id from the renderer — `sectionIdMap` then missed\n *     silently and the deep-link anchor pointed nowhere;\n *   - the extractor anchored ATX at column 0 while the renderer allowed the\n *     CommonMark 0..3-space indent;\n *   - the dedupe counter was hand-copied in both files, each with a comment\n *     saying \"the two must agree\".\n * Both now call `scanHeadings` + `createHeadingIdDeduper`. Server-safe: no\n * React, no DOM.\n */\n\n// The CommonMark fence machine used to live in THIS file, which meant a\n// streaming renderer imported its fence state from a module named after\n// heading slugs. It now lives in `./markdown-fences`; re-exported here so the\n// public `utils` surface (and every existing import) is unchanged.\nexport {\n  createFenceTracker,\n  isBlankLine,\n  type FenceTracker,\n  type FenceLineRole,\n} from './markdown-fences'\nimport { createFenceTracker, isBlankLine } from './markdown-fences'\n\n/** A heading the renderer will emit, located in the source. */\nexport interface ScannedHeading {\n  /** 1-based line of the heading's FIRST line (setext: the title line). */\n  line: number\n  level: number\n  /** Raw title text, before `stripInlineMarkdown` / slugification. */\n  text: string\n}\n\nexport interface ScanHeadingsOptions {\n  /**\n   * Skip a leading YAML frontmatter block (`---` on line 1 through the next\n   * `---`). Only a DOCUMENT-LEADING block counts: a bare `---` mid-document\n   * is a thematic break or a setext underline, and treating it as a\n   * frontmatter toggle (what the extractor used to do) silently swallowed\n   * every heading until the next one.\n   */\n  skipFrontmatter?: boolean\n  /** Include raw-HTML headings (`<h2>Title</h2>`). Default true. */\n  includeRawHtml?: boolean\n  /**\n   * Skip fenced-code blocks. Default true — a `##` inside a code fence is\n   * code, not a heading, and the renderer emits no id for it.\n   */\n  skipFences?: boolean\n}\n\n/**\n * EVERY line-anchored regex in this module ends `\\r?$`, not `$`.\n *\n * Lines here come from `content.split('\\n')`, so on a CRLF document each one\n * carries a trailing `\\r`. `$` (no `m` flag) anchors at end of string and `\\r`\n * is not in `[ \\t]`, so a `$`-anchored pattern matches NOTHING on such a\n * document. That was a live REGRESSION: `main`'s looser `^(#{1,6})\\s+(.+)`\n * had no end anchor at all and tolerated the `\\r` (the title was `.trim()`ed\n * afterwards), so `'# Real\\r\\n\\r\\nbody\\r\\n\\r\\n## Second\\r\\n'` yielded\n * `[\"Real\",\"Second\"]` on `main` and `[]` here — every CRLF-stored doc / blog /\n * release body silently lost its TOC, its in-page anchors and its doc-SEO\n * heading links.\n *\n * The `\\r` is ABSORBED, not normalized away: `scanHeadings` reports 1-based\n * LINE numbers that the renderer's id map is keyed by, and the sanitizer's mask\n * (which shares `markdown-fences`) is length-preserving — neither may be fed a\n * rewritten source.\n */\n/** CommonMark ATX heading (closing `#` run is not part of the title). */\nconst ATX_HEADING_RE = /^ {0,3}(#{1,6})[ \\t]+(.*?)(?:[ \\t]+#+)?[ \\t]*\\r?$/\n/** ATX with no title at all (`###`, `## ###`) — still a heading, empty text. */\nconst ATX_EMPTY_RE = /^ {0,3}(#{1,6})[ \\t]*#*[ \\t]*\\r?$/\n/**\n * Raw-HTML heading (`<h2 id=\"x\">Title</h2>`) — rehype-raw renders these too.\n * GLOBAL: two `<h3>`s on one line are two headings, and a non-global scan\n * silently dropped the second (it then fell through to the renderer's\n * suffix-free fallback and emitted a DUPLICATE DOM id).\n */\nconst RAW_HEADING_RE = /<h([1-6])\\b[^>]*>([\\s\\S]*?)(?:<\\/h\\1>|$)/gi\n/**\n * Blockquote markers and list-item markers preceding an ATX heading\n * (`> ## Setup`, `- ## Setup`, `1. ## Setup`). mdast emits a REAL `<h2>` for\n * these; scanning only column-0..3 missed them entirely, so two identical\n * `> ## Setup` headings both hit the suffix-free fallback and emitted the\n * duplicate DOM ids this whole module exists to prevent.\n */\nconst CONTAINER_PREFIX_RE = /^ {0,3}(?:(?:>[ \\t]?)+|(?:[-+*]|\\d{1,9}[.)])[ \\t]+)+/\n/** Setext underline (`===` → h1, `---` → h2), AFTER container-prefix strip. */\nconst SETEXT_UNDERLINE_RE = /^ {0,3}(=+|-+)[ \\t]*\\r?$/\n/** Thematic break — ends a paragraph run, never underlines it. */\nconst THEMATIC_BREAK_RE = /^ {0,3}(\\*|_){3,}[ \\t]*\\r?$/\n/**\n * CommonMark HTML-block type 6 tag names — these open a raw HTML block no\n * matter what follows them on the line.\n */\nconst HTML_BLOCK_TAGS =\n  'address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h1|h2|h3|h4|h5|h6|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul'\n\n/**\n * A line that OPENS a raw HTML block. mdast hands the whole block to the HTML\n * parser, so a `---` inside it is not a setext underline and the lines above\n * it are not a paragraph: `<div>\\nText\\n---\\n</div>` emits NO heading. The\n * scanner used to publish a phantom `Text` h2 there.\n *\n * The naive `^ {0,3}<` this replaced disqualified any run whose first line\n * merely STARTED with a tag, so `<span>x</span> more text` + `---` (a plain\n * paragraph in mdast, yielding an h2) was missed. The alternatives are spelled\n * out per CommonMark:\n *   - types 1-5: `<script|pre|style|textarea`, `<!--`, `<?`, `<!X`, `<![CDATA[`\n *   - type 6:    a `HTML_BLOCK_TAGS` name, whatever follows it\n *   - type 7:    ANY complete open/closing tag ALONE on the line (trailing\n *                whitespace only) — this is the clause the old regex got wrong\n * Attribute soup inside a type-7 tag is approximated with `[^>]*` rather than\n * a full attribute grammar; the error bias is unchanged (a missed run costs a\n * TOC entry, never a wrong id — the same direction as the documented\n * list-item-indent gap in `SetextRun.prefix`).\n */\nconst HTML_BLOCK_OPENER_RE = new RegExp(\n  '^ {0,3}(?:' +\n    // types 1-5\n    '<(?:script|pre|style|textarea)(?:[\\\\s>]|$)|<!--|<\\\\?|<![a-zA-Z]|<!\\\\[CDATA\\\\[' +\n    // type 6\n    `|</?(?:${HTML_BLOCK_TAGS})(?:[\\\\s/>]|$)` +\n    // type 7 — a complete tag standing alone on the line\n    '|<[a-zA-Z][a-zA-Z0-9-]*(?:\\\\s[^>]*)?/?>[ \\\\t]*\\\\r?$' +\n    '|</[a-zA-Z][a-zA-Z0-9-]*[ \\\\t]*>[ \\\\t]*\\\\r?$' +\n    ')',\n  'i',\n)\n\n/**\n * The paragraph RUN that a setext underline would convert into a heading.\n *\n * mdast positions a setext heading at the FIRST line of the paragraph and its\n * text is the WHOLE run (`Foo\\nbar\\n===` → `{line:1, text:\"Foo\\nbar\"}`). The\n * scanner used to remember only the LAST line, so the renderer's line-keyed\n * lookup missed the heading entirely (falling back to a slug of the rendered\n * children, `foo-bar`) while the extractor published `bar` — a dead deep\n * link, plus a phantom TOC entry at the wrong line.\n */\ninterface SetextRun {\n  /** 1-based line of the run's FIRST line — where mdast puts the heading. */\n  line: number\n  /**\n   * Container prefix (`> `, `- `, …) every line of the run shares. The\n   * underline must carry the IDENTICAL prefix, which is why `> Quote` +\n   * `> ---` scans as an h2 while `> Quote` + a bare `---` does not.\n   *\n   * KNOWN, DELIBERATE GAP: a setext underline indented to a LIST ITEM's\n   * content column (`- item` / `  ---`) is a heading in mdast but has no\n   * container prefix at all, so this equality test misses it. Closing that\n   * would mean re-deriving list-item content columns here — the scanner\n   * simply emits nothing (the pre-existing behavior), which costs a TOC entry\n   * and never produces a WRONG id.\n   */\n  prefix: string\n  /** Run lines with the container prefix stripped, in order. */\n  lines: string[]\n  /** Run opened with a raw-HTML block line — mdast emits no heading for it. */\n  disqualified: boolean\n}\n\n/**\n * Collect every heading the markdown renderer will emit, in document order.\n * THE single definition of \"which lines are headings\" for both the extractor\n * and the renderer's id map.\n */\nexport function scanHeadings(\n  content: string,\n  options: ScanHeadingsOptions = {},\n): ScannedHeading[] {\n  const { skipFrontmatter = true, includeRawHtml = true, skipFences = true } = options\n  const out: ScannedHeading[] = []\n  const lines = content.split('\\n')\n  // ONE fenced-code state machine (utils/markdown-fences), shared with\n  // markdown/streaming.ts.\n  const fences = skipFences ? createFenceTracker() : null\n\n  let start = 0\n  if (skipFrontmatter && lines[0]?.trim() === '---') {\n    for (let i = 1; i < lines.length; i++) {\n      if (lines[i].trim() === '---' || lines[i].trim() === '...') {\n        start = i + 1\n        break\n      }\n    }\n  }\n\n  // The paragraph run a setext underline would convert (see SetextRun).\n  let setextRun: SetextRun | null = null\n\n  for (let i = start; i < lines.length; i++) {\n    const line = lines[i]\n    if (fences) {\n      const role = fences.push(line)\n      if (role !== 'text') {\n        setextRun = null\n        continue\n      }\n    }\n\n    // Container prefix (`> `, `- `, `1. `) is stripped from EVERY construct\n    // below, so a blockquote/list-nested heading is scanned in both its ATX\n    // (`> ## Setup`) and setext (`> Quote title` / `> ---`) spellings.\n    const prefix = CONTAINER_PREFIX_RE.exec(line)?.[0] ?? ''\n    const body = prefix ? line.slice(prefix.length) : line\n\n    // --- setext (`Title` / `===`): a REAL h1/h2 in mdast, positioned at the\n    // run's FIRST line — which is the line the heading-id map is keyed by.\n    // The underline must carry the SAME container prefix as the run it\n    // closes; `> Quote` followed by a bare `---` is a quote and a thematic\n    // break, not a heading.\n    const setext = SETEXT_UNDERLINE_RE.exec(body)\n    if (setext) {\n      if (setextRun && !setextRun.disqualified && setextRun.prefix === prefix) {\n        out.push({\n          line: setextRun.line,\n          level: setext[1][0] === '=' ? 1 : 2,\n          text: setextRun.lines.join('\\n'),\n        })\n      }\n      setextRun = null\n      continue\n    }\n\n    // --- ATX, including inside a blockquote / list item.\n    const atx = ATX_HEADING_RE.exec(body) ?? ATX_EMPTY_RE.exec(body)\n    if (atx) {\n      out.push({ line: i + 1, level: atx[1].length, text: (atx[2] ?? '').trim() })\n      setextRun = null\n      continue\n    }\n\n    if (includeRawHtml && line.indexOf('<') !== -1) {\n      RAW_HEADING_RE.lastIndex = 0\n      let raw: RegExpExecArray | null\n      let matched = false\n      while ((raw = RAW_HEADING_RE.exec(line)) !== null) {\n        matched = true\n        out.push({\n          line: i + 1,\n          level: Number(raw[1]),\n          text: raw[2].replace(/<[^>]*>/g, '').trim(),\n        })\n      }\n      if (matched) {\n        setextRun = null\n        continue\n      }\n    }\n\n    // Paragraph accumulation. A blank line or a thematic break ends the run;\n    // a change of container prefix starts a new block (a list or blockquote\n    // interrupts a paragraph in CommonMark).\n    // CommonMark's blank line (spaces/tabs), NOT `trim()` — an NBSP-only line\n    // is paragraph CONTENT to remark, so ending the run there would move a\n    // setext heading's reported line off the run's first line.\n    if (isBlankLine(body) || THEMATIC_BREAK_RE.test(body)) {\n      setextRun = null\n      continue\n    }\n    if (setextRun && setextRun.prefix === prefix) {\n      setextRun.lines.push(body.trim())\n    } else {\n      setextRun = {\n        line: i + 1,\n        prefix,\n        lines: [body.trim()],\n        disqualified: HTML_BLOCK_OPENER_RE.test(body),\n      }\n    }\n  }\n\n  return out\n}\n\n/**\n * The duplicate-id suffix counter, as ONE closure both sides instantiate.\n * First occurrence keeps the bare slug; the Nth gets `-N`. Extractor and\n * renderer MUST use the same counter or `#anchor` deep links diverge.\n */\nexport function createHeadingIdDeduper(): (cleanId: string) => string {\n  const counts: Record<string, number> = {}\n  return (cleanId: string): string => {\n    if (counts[cleanId]) {\n      counts[cleanId]++\n      return `${cleanId}-${counts[cleanId]}`\n    }\n    counts[cleanId] = 1\n    return cleanId\n  }\n}\n","/**\n * GitHub-compatible anchor resolution for in-page `#hash` links.\n *\n * ## The mismatch this exists to bridge\n *\n * Doc authors write their table of contents against GitHub's slugger, which\n * keeps the separator left behind by a stripped emoji:\n *\n *   `## 📚 Table of Contents`  →  GitHub anchor `#-table-of-contents`\n *\n * Our heading ids come from the shared slug SSOT (`markdown-heading-id`),\n * whose `slugifyHeadingBase` trims leading and trailing hyphens — so the same\n * heading renders as `id=\"table-of-contents\"`. The \"On this page\" rail\n * navigates by those ids and works; the in-body TOC link points at an id that\n * does not exist and the browser silently does nothing.\n *\n * Rather than change the id shape (it is already baked into shared URLs and\n * the rail), resolution is made tolerant: compare a *normalized* form of both\n * sides. This module owns ONLY that tolerance — the slug chain itself is\n * imported, never re-implemented, so a change to the id shape lands in one\n * place and both sides move together.\n *\n * Callers do not reach for this directly: `getHashTargetElement`\n * (`utils/same-page-hash-nav`) is the ONE resolver every hash consumer goes\n * through, and it applies this as its last pass.\n */\nimport { slugifyHeadingText } from './markdown-heading-id'\n\n/**\n * Reduce an anchor id (ours or GitHub's) to a comparable form.\n *\n * Everything but the final hyphen-run collapse is the shared heading-slug\n * chain: emoji strip → lowercase → drop non-word chars → spaces to hyphens →\n * trim edge hyphens. The collapse is this module's own addition, covering\n * GitHub's *interior* divergence — `## 💬 Community & Support` slugs to\n * `#-community--support` (a double hyphen around the dropped `&`) against our\n * `community-support`.\n *\n *   `'-getting-started'`     → `'getting-started'`\n *   `'-community--support'`  → `'community-support'`\n *   `'Getting Started'`      → `'getting-started'`\n */\nexport function normalizeAnchorId(raw: string): string {\n  return slugifyHeadingText(raw).replace(/-+/g, '-').replace(/^-+|-+$/g, '')\n}\n\n/** Heading elements carrying an id — the candidate set for the fuzzy pass. */\nconst HEADING_SELECTOR = 'h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]'\n\n/** A `base-N` id, split into its base and ordinal. */\nconst DUPLICATE_SUFFIX_RE = /^(.+)-(\\d+)$/\n\n/**\n * The two sides also disagree on where duplicate-heading numbering STARTS.\n * For a document with two `## Setup` headings GitHub emits `setup` and\n * `setup-1`, while `createHeadingIdDeduper` emits `setup` and `setup-2` (its\n * suffix is the occurrence COUNT, not the offset). So a GitHub `#setup-1`\n * needs our `setup-2` — every duplicate is off by exactly one.\n *\n * Runs LAST, after the exact lookup and the heading scan have both missed: a\n * heading that genuinely slugs to `step-2` is resolved long before this, and\n * the shift is the one pass that can match an id it was not asked for.\n */\nfunction findDuplicateShifted(normalized: string, doc: Document): HTMLElement | null {\n  const parts = DUPLICATE_SUFFIX_RE.exec(normalized)\n  if (!parts) return null\n  const ordinal = Number(parts[2])\n  if (!Number.isSafeInteger(ordinal)) return null\n  return doc.getElementById(`${parts[1]}-${ordinal + 1}`)\n}\n\n/**\n * Resolve a `#hash` id that no exact lookup matched, by comparing normalized\n * forms. Returns `null` when nothing in the document matches, so the caller\n * can leave the browser's default behavior alone rather than hijacking a\n * click that was never ours to handle.\n *\n * Three passes, in order of how much they assume:\n *   1. a single `getElementById` on the normalized id — catches the whole\n *      emoji-hyphen family without walking the DOM;\n *   2. a heading scan — catches ids whose own raw form normalizes differently\n *      (e.g. `a---b`, from a title containing a literal ` - `);\n *   3. the duplicate-heading shift (see `findDuplicateShifted`) — last,\n *      because it is the only pass that resolves to an id the fragment did\n *      not name.\n */\nexport function findAnchorElementByNormalizedId(\n  rawId: string,\n  doc: Document,\n): HTMLElement | null {\n  const normalized = normalizeAnchorId(rawId)\n  if (!normalized) return null\n\n  if (normalized !== rawId) {\n    const direct = doc.getElementById(normalized)\n    if (direct) return direct\n  }\n\n  for (const heading of Array.from(doc.querySelectorAll<HTMLElement>(HEADING_SELECTOR))) {\n    if (normalizeAnchorId(heading.id) === normalized) return heading\n  }\n\n  // Last: the shift would happily match an unrelated `x-3` for a `x-2` that\n  // has no heading at all, so it runs only once the scan has come up empty.\n  return findDuplicateShifted(normalized, doc)\n}\n","import { scrollElementIntoView } from './scroll-into-view'\nimport { findAnchorElementByNormalizedId } from './anchor-id'\n\n/** Pages with a section-nav STRIP on top of the global hub header\n *  (dev-center roadmap/delivery/tickets, FAQ category-pill nav).\n *  Anchor lands BELOW both layers. */\nexport const STICKY_HEADER_OFFSET_PX = 96\n\n/** Pages with only the global hub header (docs, blog, vendor detail).\n *  Anchor lands BELOW the header bar. */\nexport const HUB_HEADER_OFFSET_PX = 80\n\n/**\n * Take only the FIRST hash segment from a fragment that may contain extra\n * `#` characters. `'' → ''`, `'#a' → '#a'`, `'#a#b' → '#a'`.\n *\n * No real DOM id contains `#`, so a multi-fragment hash is always a bug at\n * the composer site; `navigateSamePageHash` + `useScrollToHash` both call\n * this so URL bar and `getElementById` stay in sync.\n */\nexport function normalizeHashFragment(hash: string): string {\n  if (!hash) return ''\n  const second = hash.indexOf('#', 1)\n  return second < 0 ? hash : hash.slice(0, second)\n}\n\n/**\n * Resolve the DOM element a hash fragment id points at — THE hash resolver;\n * `navigateSamePageHash`, `useScrollToHash` and the markdown link renderer\n * all go through this one, so a fragment that resolves for a deep link\n * resolves for an in-body click too.\n *\n * Three passes, cheapest first:\n *   1. the raw id (ordinary anchors — the overwhelming common case);\n *   2. the percent-decoded form — href composers like `buildTicketOpenHref`\n *      encode the id into the fragment while rows render the raw id, so\n *      `#ticket-a%2Fb` must still find `id=\"ticket-a/b\"`. A malformed escape\n *      sequence just falls through;\n *   3. a NORMALIZED match against the document's headings — doc bodies are\n *      authored on GitHub, whose slugger keeps the hyphen left behind by a\n *      stripped emoji (`## 📚 Table of Contents` → `#-table-of-contents`)\n *      while our ids trim it. Without this every in-body TOC link in every\n *      GitHub-authored doc resolves to nothing and clicking one does nothing.\n *      See `utils/anchor-id`.\n */\nexport function getHashTargetElement(id: string): HTMLElement | null {\n  if (!id) return null\n  const direct = document.getElementById(id)\n  if (direct) return direct\n\n  let decoded = id\n  try {\n    decoded = decodeURIComponent(id)\n    if (decoded !== id) {\n      const decodedEl = document.getElementById(decoded)\n      if (decodedEl) return decodedEl\n    }\n  } catch {\n    // malformed escape — the raw lookup above already covered it\n    decoded = id\n  }\n\n  return findAnchorElementByNormalizedId(decoded, document)\n}\n\nexport interface NavigateSamePageHashOptions {\n  /** Pixels to subtract for sticky chrome. */\n  headerOffset?: number\n  /** `'push'` (default) — new history entry; `'replace'` — overwrite\n   *  current entry (use for TOC-style in-page navigators). */\n  history?: 'push' | 'replace'\n}\n\n/**\n * Same-page hash navigation primitive: pushState + synthetic `hashchange`\n * + anchoring-proof smooth scroll. Replaces `router.push` for hash CTAs\n * (Next.js suppresses smooth-scroll during navigation; `router.push` on\n * an exact-URL match is a no-op). Returns `true` when the helper claimed\n * the nav (same pathname + search); `false` for cross-page targets so\n * callers fall through to `router.push`.\n *\n * `target` accepts an origin-stripped path (`/x#anchor`) or a bare hash\n * (`#anchor`); bare-hash callers don't need to reconstruct `pathname +\n * search` themselves.\n */\nexport function navigateSamePageHash(\n  target: string,\n  options: NavigateSamePageHashOptions = {},\n): boolean {\n  if (typeof window === 'undefined') return false\n  const { headerOffset = 0, history: historyMode = 'push' } = options\n  const normalizedTarget =\n    target.startsWith('#')\n      ? window.location.pathname + window.location.search + target\n      : target\n  // `new URL(absoluteUrl, base)` ignores `base` per RFC 3986; an absolute\n  // cross-origin target sharing pathname/search would otherwise pass the\n  // check below and trip pushState's same-origin enforcement. Parse with\n  // an explicit base so malformed inputs cleanly fall through.\n  let url: URL\n  try {\n    url = new URL(normalizedTarget, window.location.href)\n  } catch {\n    return false\n  }\n  if (\n    url.origin !== window.location.origin ||\n    url.pathname !== window.location.pathname ||\n    url.search !== window.location.search\n  ) {\n    return false\n  }\n  const current = window.location.pathname + window.location.search + window.location.hash\n  // Heal a malformed multi-fragment hash so the URL bar is clean and\n  // `getElementById` resolves. Dev-warn fingers the upstream composer.\n  const normalizedHash = normalizeHashFragment(url.hash)\n  if (process.env.NODE_ENV === 'development' && normalizedHash !== url.hash) {\n    // eslint-disable-next-line no-console\n    console.warn(\n      `[navigateSamePageHash] malformed fragment \"${url.hash}\" → normalizing to \"${normalizedHash}\". Fix the upstream composer.`,\n    )\n  }\n  const next = url.pathname + url.search + normalizedHash\n  const id = normalizedHash && normalizedHash !== '#' ? normalizedHash.slice(1) : ''\n  // Hash-less targets are only ours on an EXACT URL re-click.\n  if (!id && next !== current) return false\n  if (next !== current) {\n    const oldURL = window.location.href\n    if (historyMode === 'replace') {\n      window.history.replaceState(null, '', next)\n    } else {\n      window.history.pushState(null, '', next)\n    }\n    // Synthetic `hashchange` — `pushState` doesn't fire it (HTML spec),\n    // so URL-hash-bound listeners (FAQ auto-expand, etc.) wouldn't react.\n    window.dispatchEvent(new HashChangeEvent('hashchange', {\n      oldURL,\n      newURL: window.location.href,\n    }))\n  }\n  const el = id ? getHashTargetElement(id) : null\n  if (id && !el && process.env.NODE_ENV === 'development') {\n    // eslint-disable-next-line no-console\n    console.warn(\n      `[navigateSamePageHash] anchor \"#${id}\" not found — scrolling to top.`,\n    )\n  }\n  // Missing anchor → tween to page top. `documentElement` is at 0 by\n  // definition, so one tween covers both branches.\n  scrollElementIntoView(el ?? document.documentElement, {\n    behavior: 'smooth',\n    headerOffset,\n  })\n  return true\n}\n","'use client'\n\nimport { useCallback, useRef } from 'react'\nimport {\n  HONEYPOT_FIELD,\n  ELAPSED_MS_FIELD,\n  type HumanitySignals,\n} from '../utils/humanity-signals'\n\n/**\n * useHumanitySignals — client primitive backing the invisible bot-protection\n * layer. Owns the honeypot <input> ref + a mount timestamp, and exposes the\n * props to render the field plus a getter that produces the wire object to\n * merge into a form's POST body. Reused by every public form (contact,\n * waitlist, ticket, review) so the signal shape lives in exactly one place.\n *\n * Usage:\n *   const { honeypotInputProps, getSignals, resetSignals } = useHumanitySignals()\n *   // render: <HoneypotField {...honeypotInputProps} />\n *   // submit: fetch(url, { body: JSON.stringify({ ...formData, ...getSignals() }) })\n *   // after a successful submit: resetSignals()\n *\n * The two signals are origin-independent (they ride in the body), so they keep\n * working when the form is embedded behind a reverse-proxy / prefixed URL.\n */\nexport function useHumanitySignals() {\n  const ref = useRef<HTMLInputElement>(null)\n  // performance.now() is monotonic (immune to wall-clock skew). SSR-guarded\n  // even though the hook only executes on the client.\n  const mountedAt = useRef<number>(typeof performance !== 'undefined' ? performance.now() : 0)\n\n  const getSignals = useCallback(\n    (): HumanitySignals => ({\n      [HONEYPOT_FIELD]: ref.current?.value ?? '',\n      [ELAPSED_MS_FIELD]:\n        typeof performance !== 'undefined' ? Math.round(performance.now() - mountedAt.current) : 0,\n    }),\n    [],\n  )\n\n  const resetSignals = useCallback(() => {\n    if (ref.current) ref.current.value = ''\n    if (typeof performance !== 'undefined') mountedAt.current = performance.now()\n  }, [])\n\n  return { honeypotInputProps: { ref, name: HONEYPOT_FIELD }, getSignals, resetSignals } as const\n}\n","/**\n * Humanity signals — invisible bot-protection primitives shared by the lib's\n * public forms (client) and the hub's per-route `verifyHuman` gate (server).\n *\n * PURE + React-free on purpose: this module is a tsup SERVER entry (no\n * \"use client\" banner) so the hub can import it server-side without pulling a\n * client-reference boundary — same pattern as `schemas/contact-schema` and\n * `components/features/mux-origins`.\n *\n * Two origin-independent signals travel in the POST body: a honeypot (a hidden\n * field real users never fill) and timing (ms from form mount to submit).\n * `evaluateHumanitySignals` is the SINGLE source of truth for the block/allow\n * decision — the hub imports + calls it rather than re-implementing the rules.\n */\n\n/** Hidden honeypot field name. Innocuous + autofill-resistant (deliberately NOT name/email). */\nexport const HONEYPOT_FIELD = 'contact_url_confirm'\n/** Client-measured ms between form mount and submit. */\nexport const ELAPSED_MS_FIELD = 'form_elapsed_ms'\n/** Default minimum fill time (ms). A submit faster than this is treated as a bot. */\nexport const DEFAULT_MIN_FILL_MS = 700\n\n/**\n * Every humanity-signal key that rides in a public form's POST body.\n * Server-side handlers that forward form payloads upstream (HubSpot booking,\n * CRM pushes, …) MUST strip by THIS array — never hand-typed strings — so a\n * field rename here propagates everywhere and the honeypot value can never\n * silently leak into an upstream record.\n */\nexport const HUMANITY_SIGNAL_KEYS = [HONEYPOT_FIELD, ELAPSED_MS_FIELD] as const\n\n/** Keyed wire object produced by `useHumanitySignals().getSignals()` and spread into the POST body. */\nexport type HumanitySignals = Record<string, string | number>\n\n/** Result of {@link evaluateHumanitySignals}. */\nexport type HumanityVerdict = { ok: true } | { ok: false; reason: 'honeypot' | 'too_fast' }\n\n/** Tolerant reader — never throws; missing/garbage timing → null. */\nexport function extractHumanitySignals(body: unknown): { honeypot: string; elapsedMs: number | null } {\n  const b = (body ?? {}) as Record<string, unknown>\n  const rawHp = b[HONEYPOT_FIELD]\n  // A legit client always sends a STRING here (getSignals → ref.value ?? ''),\n  // so ANY present non-string value is a bot filling the decoy with a non-string\n  // to dodge the empty-check — coerce to a (non-empty) string so it still trips.\n  // null/undefined → '' = the correct \"field absent / unfilled\" allow case.\n  const honeypot = rawHp == null ? '' : String(rawHp)\n  const rawMs = b[ELAPSED_MS_FIELD]\n  const elapsedMs = typeof rawMs === 'number' && Number.isFinite(rawMs) ? rawMs : null\n  return { honeypot, elapsedMs }\n}\n\n/**\n * SINGLE decision fn for honeypot + timing (the hub's `verifyHuman` imports + calls this):\n * - honeypot non-empty → bot (real users never fill the off-screen field)\n * - elapsed below `minFillMs` → bot (humans take time; a MISSING timing value never blocks)\n */\nexport function evaluateHumanitySignals(body: unknown, opts: { minFillMs: number }): HumanityVerdict {\n  const { honeypot, elapsedMs } = extractHumanitySignals(body)\n  if (honeypot.trim() !== '') return { ok: false, reason: 'honeypot' }\n  if (elapsedMs !== null && elapsedMs < opts.minFillMs) return { ok: false, reason: 'too_fast' }\n  return { ok: true }\n}\n\n/** Parse a comma-separated env string → trimmed, non-empty entries (undefined → []). */\nexport const splitCsvEnv = (s?: string): string[] =>\n  s?.split(',').map((t) => t.trim()).filter(Boolean) ?? []\n"]}