{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-broadcast-channel",
  "title": "useBroadcastChannel",
  "description": "Broadcasts messages between same-origin tabs over BroadcastChannel, with SSR-safe capability detection, an optional local echo and structured-clone diagnostics.",
  "files": [
    {
      "path": "src/registry/hooks/use-broadcast-channel.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nexport interface UseBroadcastChannelOptions<T> {\n  /**\n   * Called on every incoming message. `meta.local` tells you where it came from:\n   * `false` = another tab/window/worker, `true` = this page's own `post` replayed by\n   * `echo` (see below).\n   *\n   * Kept in a latest-ref, synced on every render and **never in an effect dependency\n   * array** — consumers pass an inline arrow (the common case), which is a new reference\n   * every render, and depending on it would close/re-open the channel constantly,\n   * dropping every message that arrives during the gap.\n   */\n  onMessage?: (data: T, meta: { local: boolean }) => void\n  /**\n   * Called when a message arrives that **cannot be deserialized** (the native\n   * `messageerror` event: the sender could structured-clone it, the receiver cannot put\n   * it back together — usually a type mismatch across browser versions). Without a\n   * handler it goes to `console.error` — this path is never swallowed, or messages\n   * disappear with nothing to debug.\n   */\n  onMessageError?: (event: MessageEvent) => void\n  /**\n   * On `post`, **also replay the message into this page** (calls\n   * `onMessage(data, { local: true })` and updates `lastMessage`). Defaults to `false`,\n   * i.e. BroadcastChannel's native semantics: **the sender never receives its own\n   * message**.\n   *\n   * Off is the right default — the initiator has normally already changed its own state\n   * in place (the tab you clicked \"log out\" in logs itself out; the broadcast is for\n   * \"everyone else\"), so replaying would run the same work twice. Turn it on only when\n   * local state is driven **entirely** by the channel (a single entry point).\n   *\n   * One asymmetry to know about: an echo replays **the same object reference** (it never\n   * goes through structured clone), while remote receivers get a copy. So never mutate a\n   * received object inside `onMessage` — locally you would also mutate the caller's copy,\n   * remotely you would not, and the two sides diverge. Treat messages as immutable.\n   */\n  echo?: boolean\n}\n\nexport interface UseBroadcastChannelResult<T> {\n  /**\n   * Broadcast a message to the other same-origin BroadcastChannel instances. With `echo`\n   * on, it is replayed into this page too.\n   *\n   * Reference-stable (changes only with `name`), so it is safe in a dependency array.\n   *\n   * When `isSupported === false` (old browsers / during SSR) it silently does nothing —\n   * degrading to \"this sync did not happen\", with no throw and no guard at every call\n   * site; read `isSupported` when you need an explicit branch. `echo` still applies\n   * though: it means \"deliver to me as well\", which has nothing to do with transport.\n   *\n   * Passing data that is **not structured-cloneable** (functions, DOM nodes, class\n   * instances carrying methods, Proxies) **throws an Error with diagnostics** (the\n   * original DataCloneError hangs off `cause`). That is a caller bug and must not be\n   * swallowed.\n   */\n  post: (data: T) => void\n  /** Whether this environment has BroadcastChannel. Always `false` during SSR and on the hydration frame. */\n  isSupported: boolean\n  /** The most recent message received (or echoed locally); `undefined` until one arrives. Changing `name` clears it. */\n  lastMessage: T | undefined\n}\n\n/** Capability detection via useSyncExternalStore: an \"external value\" that never changes, so subscribe has nothing to subscribe to. */\nconst subscribeToNothing = () => () => {}\n\nconst getSupportedSnapshot = () => typeof BroadcastChannel !== \"undefined\"\n\n/**\n * The server snapshot is hard-coded to false — and this is not ceremony: **Node 18+ ships\n * a global `BroadcastChannel`** (from `node:worker_threads`), so during SSR\n * `getSupportedSnapshot()` returns true while pointing at an in-process channel that has\n * nothing to do with the browser. A separate server snapshot pins it to false, so the\n * first frame always renders the degraded UI.\n */\nconst getSupportedServerSnapshot = () => false\n\n/**\n * Broadcast messages between same-origin tabs: one tab logs out, the rest follow; the\n * theme changes in one place and syncs everywhere. Built on the native BroadcastChannel,\n * this hook absorbs the boilerplate — opening the channel, subscribing, closing on\n * unmount, capability detection and clone-failure diagnostics.\n *\n * - **The sender never receives its own message** — that is BroadcastChannel's own\n *   behaviour, not a choice made here. Turn on `echo` when this page must react too (see\n *   `UseBroadcastChannelOptions.echo`). Note that \"another hook instance in the same page\"\n *   counts as **another** channel object and does receive it: the only instance left out\n *   is the one that called `post`.\n * - **Capability detection never happens during render**: `useSyncExternalStore` with a\n *   `false` server fallback, so the render body touches no browser global and there is no\n *   hydration mismatch.\n * - **The channel is only rebuilt when `name` changes**; callbacks live in a latest-ref\n *   and stay out of the dependencies.\n * - On unmount, `removeEventListener` + `channel.close()` — nothing left behind.\n */\nexport function useBroadcastChannel<T = unknown>(\n  name: string,\n  options: UseBroadcastChannelOptions<T> = {},\n): UseBroadcastChannelResult<T> {\n  const { onMessage, onMessageError, echo = false } = options\n\n  const isSupported = React.useSyncExternalStore(\n    subscribeToNothing,\n    getSupportedSnapshot,\n    getSupportedServerSnapshot,\n  )\n\n  const [lastMessage, setLastMessage] = React.useState<T | undefined>(undefined)\n\n  // a new `name` is a different channel, so the previous lastMessage is meaningless.\n  // \"reset state when an input changes\" is done with a render-phase adjust-state rather\n  // than setState in an effect — the officially sanctioned pattern, and it keeps\n  // react-hooks/set-state-in-effect quiet.\n  const [trackedName, setTrackedName] = React.useState(name)\n  if (trackedName !== name) {\n    setTrackedName(name)\n    setLastMessage(undefined)\n  }\n\n  const onMessageRef = React.useRef(onMessage)\n  const onMessageErrorRef = React.useRef(onMessageError)\n  const echoRef = React.useRef(echo)\n  // sync the latest values after every render. writing ref.current rather than state\n  // triggers no re-render and keeps the dependency arrays below clean.\n  React.useEffect(() => {\n    onMessageRef.current = onMessage\n    onMessageErrorRef.current = onMessageError\n    echoRef.current = echo\n  })\n\n  const channelRef = React.useRef<BroadcastChannel | null>(null)\n\n  // remote messages and local echo replays share one delivery path, so they look identical to consumers.\n  const deliver = React.useCallback((data: T, local: boolean) => {\n    setLastMessage(data)\n    onMessageRef.current?.(data, { local })\n  }, [])\n\n  React.useEffect(() => {\n    if (typeof BroadcastChannel === \"undefined\") return\n\n    const channel = new BroadcastChannel(name)\n    channelRef.current = channel\n\n    const handleMessage = (event: MessageEvent) => {\n      deliver(event.data as T, false)\n    }\n\n    const handleMessageError = (event: MessageEvent) => {\n      const handler = onMessageErrorRef.current\n      if (handler) {\n        handler(event)\n        return\n      }\n      console.error(\n        `useBroadcastChannel(\"${name}\"): a message arrived on this channel but could not be ` +\n          \"deserialized (native 'messageerror'). The payload was cloneable on the sending side \" +\n          \"but not reconstructible here — pass onMessageError to handle it explicitly.\",\n        event,\n      )\n    }\n\n    channel.addEventListener(\"message\", handleMessage)\n    channel.addEventListener(\"messageerror\", handleMessageError)\n\n    return () => {\n      channel.removeEventListener(\"message\", handleMessage)\n      channel.removeEventListener(\"messageerror\", handleMessageError)\n      channel.close()\n      channelRef.current = null\n    }\n  }, [name, deliver])\n\n  const post = React.useCallback(\n    (data: T) => {\n      const channel = channelRef.current\n      if (channel) {\n        try {\n          channel.postMessage(data)\n        } catch (error) {\n          throw new Error(\n            `useBroadcastChannel(\"${name}\"): postMessage failed — the payload is not ` +\n              \"structured-cloneable. Functions, DOM nodes, class instances carrying methods and \" +\n              \"Proxies cannot cross a BroadcastChannel; send plain data (objects, arrays, strings, \" +\n              \"numbers, Date, Map, Set, ArrayBuffer) instead.\",\n            { cause: error },\n          )\n        }\n      }\n      if (echoRef.current) deliver(data, true)\n    },\n    [deliver, name],\n  )\n\n  return { post, isSupported, lastMessage }\n}\n\nexport default useBroadcastChannel\n",
      "type": "registry:hook"
    }
  ],
  "type": "registry:hook"
}