{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-local-storage",
  "title": "useLocalStorage",
  "description": "An SSR-safe localStorage-backed state hook, kept in sync across same-tab instances and other tabs.",
  "files": [
    {
      "path": "src/registry/hooks/use-local-storage.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\n/**\n * Custom event name used to sync instances inside one tab. The native \"storage\" event\n * only fires in *other* tabs/windows — the document that performed the write never\n * hears its own storage event — so multiple `useLocalStorage(key)` instances in one\n * tab notify each other through this custom event.\n */\nconst SYNC_EVENT = \"zyeon:local-storage-sync\"\n\ninterface SyncEventDetail {\n  key: string\n}\n\nfunction readRawValue(key: string): string | null {\n  try {\n    return window.localStorage.getItem(key)\n  } catch {\n    // in private mode, or with storage disabled outright, touching localStorage can itself throw\n    return null\n  }\n}\n\nfunction parseOrFallback<T>(raw: string | null, fallback: T): T {\n  if (raw === null) return fallback\n  try {\n    return JSON.parse(raw) as T\n  } catch {\n    // hand-edited or corrupted storage: fall back to the default, never throw\n    return fallback\n  }\n}\n\n/**\n * Per-key cache of \"the raw string last read + what it parsed to\".\n *\n * This is the load-bearing piece of the hook: `useSyncExternalStore` requires that,\n * while the store has not changed, two calls to `getSnapshot` return the `===` same\n * reference. `JSON.parse` allocates a fresh object/array every time, so re-parsing on\n * every `getSnapshot` makes React believe the store changed on every render and\n * re-render forever. Caching the raw string together with its parsed result means we\n * re-parse only when the raw string actually changed, and return the same reference\n * otherwise.\n */\nconst snapshotCache = new Map<string, { raw: string | null; value: unknown }>()\n\nfunction getSnapshotFor<T>(key: string, fallback: T): T {\n  const raw = readRawValue(key)\n  // key absent: return this instance's own initialValue (held in a per-instance ref,\n  // so the reference is stable) and do not write the shared cache — otherwise, with two\n  // instances on the same key but different defaults, the one mounted later reads the\n  // earlier one's cached default.\n  if (raw === null) return fallback\n  const cached = snapshotCache.get(key)\n  if (cached && cached.raw === raw) {\n    return cached.value as T\n  }\n  const value = parseOrFallback(raw, fallback)\n  snapshotCache.set(key, { raw, value })\n  return value\n}\n\nfunction dispatchSync(key: string) {\n  window.dispatchEvent(new CustomEvent<SyncEventDetail>(SYNC_EVENT, { detail: { key } }))\n}\n\n/**\n * State backed by localStorage, with the same call signature as `useState`:\n * `[value, setValue]`. Every `useLocalStorage` instance on the same key stays in sync,\n * whether it lives in this tab or another tab/window.\n *\n * - **SSR-safe**: the server and the client's first frame both render `initialValue`;\n *   after mount `useSyncExternalStore` switches to the real value in storage, with no\n *   extra `useEffect` and therefore no setState inside an effect body.\n * - **Stable snapshot reference**: see the comments on `getSnapshotFor` and\n *   `snapshotCache` — the biggest trap in this hook, and an infinite re-render if you\n *   get it wrong.\n * - `initialValue` is captured once at mount (same semantics as `useState`); passing a\n *   different `initialValue` later does not overwrite an existing value.\n * - A failed parse (hand-edited or corrupted storage) never throws — it falls back to\n *   `initialValue` silently.\n * - `setValue` takes a value or a `(prev) => next` updater, exactly like `useState`.\n */\nexport function useLocalStorage<T>(\n  key: string,\n  initialValue: T,\n): [T, (value: T | ((prev: T) => T)) => void] {\n  const fallbackRef = React.useRef(initialValue)\n\n  const subscribe = React.useCallback(\n    (onStoreChange: () => void) => {\n      const handleStorage = (event: StorageEvent) => {\n        // event.key === null means localStorage.clear() — every key has to refresh\n        if (event.key === key || event.key === null) onStoreChange()\n      }\n      const handleSync = (event: Event) => {\n        if ((event as CustomEvent<SyncEventDetail>).detail?.key === key) onStoreChange()\n      }\n      window.addEventListener(\"storage\", handleStorage)\n      window.addEventListener(SYNC_EVENT, handleSync)\n      return () => {\n        window.removeEventListener(\"storage\", handleStorage)\n        window.removeEventListener(SYNC_EVENT, handleSync)\n      }\n    },\n    [key],\n  )\n\n  const getSnapshot = React.useCallback(() => getSnapshotFor(key, fallbackRef.current), [key])\n  const getServerSnapshot = React.useCallback(() => fallbackRef.current, [])\n\n  const value = React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)\n\n  const setValue = React.useCallback(\n    (next: T | ((prev: T) => T)) => {\n      if (typeof window === \"undefined\") return\n      const resolved =\n        typeof next === \"function\"\n          ? (next as (prev: T) => T)(getSnapshotFor(key, fallbackRef.current))\n          : next\n      try {\n        window.localStorage.setItem(key, JSON.stringify(resolved))\n      } catch {\n        // quota exceeded, private mode blocking writes: fail silently, do not throw\n        return\n      }\n      dispatchSync(key)\n    },\n    [key],\n  )\n\n  return [value, setValue]\n}\n\nexport default useLocalStorage\n",
      "type": "registry:hook"
    }
  ],
  "type": "registry:hook"
}