{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "realtime-flow-tanstack",
  "title": "Realtime Flow",
  "description": "Real-time flow diagram editor for collaborative applications.",
  "dependencies": [
    "@xyflow/react@latest",
    "@supabase-labs/y-supabase@latest",
    "y-protocols@latest",
    "yjs@latest",
    "@supabase/ssr@latest",
    "@supabase/supabase-js@latest"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/blocks/realtime-flow/hooks/use-realtime-flow.ts",
      "content": "'use client'\n\nimport { type SupabasePersistenceOptions, SupabaseProvider } from '@supabase-labs/y-supabase'\nimport {\n  type Edge,\n  type Node,\n  type NodeChange,\n  type EdgeChange,\n  type Connection,\n  applyNodeChanges,\n  applyEdgeChanges,\n  addEdge,\n} from '@xyflow/react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { Awareness } from 'y-protocols/awareness'\nimport * as Y from 'yjs'\n\nimport { createClient } from '@/registry/default/clients/tanstack/lib/supabase/client'\n\ntype UseRealtimeFlowOptions = {\n  channel: string\n  persistence?: boolean | SupabasePersistenceOptions\n  awareness?: boolean | Awareness\n  initialNodes?: Node[]\n  initialEdges?: Edge[]\n}\n\ntype SetStateAction<T> = T | ((prev: T) => T)\n\nconst INITIAL_SYNC_MS = 350\n\nexport function useRealtimeFlow({\n  channel,\n  persistence,\n  awareness = true,\n  initialNodes = [],\n  initialEdges = [],\n}: UseRealtimeFlowOptions) {\n  const [nodes, setNodesState] = useState<Node[]>([])\n  const [edges, setEdgesState] = useState<Edge[]>([])\n  const [synced, setSynced] = useState(false)\n  const [syncError, setSyncError] = useState<string | null>(null)\n\n  const docRef = useRef<Y.Doc | null>(null)\n  const providerRef = useRef<SupabaseProvider | null>(null)\n  const syncedRef = useRef(false)\n  const initialNodesRef = useRef(initialNodes)\n  const initialEdgesRef = useRef(initialEdges)\n\n  useEffect(() => {\n    const doc = new Y.Doc()\n    const yNodes = doc.getMap<string>('nodes')\n    const yEdges = doc.getMap<string>('edges')\n    const initialNodesForChannel = initialNodesRef.current\n    const initialEdgesForChannel = initialEdgesRef.current\n    let initialSyncTimer: ReturnType<typeof setTimeout> | null = null\n    let hasRemoteActivity = false\n\n    const supabase = createClient()\n    const provider = new SupabaseProvider(channel, doc, supabase as any, {\n      awareness,\n      persistence,\n    })\n\n    // Sync Y.Map state to React state\n    const syncNodesFromYjs = () => {\n      const parsed: Node[] = []\n      yNodes.forEach((value) => {\n        try {\n          parsed.push(JSON.parse(value))\n        } catch {}\n      })\n      setNodesState(parsed)\n    }\n\n    const syncEdgesFromYjs = () => {\n      const parsed: Edge[] = []\n      yEdges.forEach((value) => {\n        try {\n          parsed.push(JSON.parse(value))\n        } catch {}\n      })\n      setEdgesState(parsed)\n    }\n\n    const clearInitialSyncTimer = () => {\n      if (initialSyncTimer) {\n        clearTimeout(initialSyncTimer)\n        initialSyncTimer = null\n      }\n    }\n\n    const clearSyncError = () => {\n      setSyncError(null)\n    }\n\n    const seedInitialState = () => {\n      if (yNodes.size === 0 && initialNodesForChannel.length > 0) {\n        doc.transact(() => {\n          for (const node of initialNodesForChannel) {\n            yNodes.set(node.id, JSON.stringify(node))\n          }\n        }, 'local')\n      }\n\n      if (yEdges.size === 0 && initialEdgesForChannel.length > 0) {\n        doc.transact(() => {\n          for (const edge of initialEdgesForChannel) {\n            yEdges.set(edge.id, JSON.stringify(edge))\n          }\n        }, 'local')\n      }\n    }\n\n    const nodesObserver = (event: Y.YMapEvent<string>) => {\n      if (event.transaction.origin === 'local') return\n      hasRemoteActivity = true\n      syncNodesFromYjs()\n    }\n\n    const edgesObserver = (event: Y.YMapEvent<string>) => {\n      if (event.transaction.origin === 'local') return\n      hasRemoteActivity = true\n      syncEdgesFromYjs()\n    }\n\n    yNodes.observe(nodesObserver)\n    yEdges.observe(edgesObserver)\n\n    const markSynced = (shouldSeed = false) => {\n      if (syncedRef.current) return\n      clearInitialSyncTimer()\n      clearSyncError()\n      syncedRef.current = true\n\n      if (shouldSeed && !hasRemoteActivity) {\n        seedInitialState()\n      }\n\n      // Sync React state from Yjs (includes seeded initial data if applied)\n      syncNodesFromYjs()\n      syncEdgesFromYjs()\n      setSynced(true)\n    }\n\n    const markSyncError = (error: Error) => {\n      clearInitialSyncTimer()\n      if (syncedRef.current) return\n      setSyncError(error.message || 'Failed to sync flow')\n    }\n\n    const persistenceInstance = provider.getPersistence()\n    if (persistenceInstance) {\n      // With persistence: wait for persisted state to load\n      if (persistenceInstance.synced) {\n        markSynced()\n      } else {\n        persistenceInstance.on('synced', () => {\n          markSynced()\n        })\n        persistenceInstance.on('error', (error) => {\n          markSyncError(error)\n        })\n      }\n    } else {\n      provider.on('connect', () => {\n        clearSyncError()\n        clearInitialSyncTimer()\n        initialSyncTimer = setTimeout(() => {\n          markSynced(true)\n        }, INITIAL_SYNC_MS)\n      })\n      provider.on('message', () => {\n        hasRemoteActivity = true\n        markSynced()\n      })\n      provider.on('error', (error) => {\n        markSyncError(error)\n      })\n    }\n\n    docRef.current = doc\n    providerRef.current = provider\n\n    return () => {\n      clearInitialSyncTimer()\n      yNodes.unobserve(nodesObserver)\n      yEdges.unobserve(edgesObserver)\n      provider.destroy()\n      doc.destroy()\n      docRef.current = null\n      providerRef.current = null\n      syncedRef.current = false\n      setSynced(false)\n      setSyncError(null)\n    }\n  }, [channel, awareness, persistence])\n\n  const syncNodesToYjs = useCallback((updated: Node[]) => {\n    const doc = docRef.current\n    if (!doc || !syncedRef.current) return\n\n    const yNodes = doc.getMap<string>('nodes')\n    doc.transact(() => {\n      const newIds = new Set(updated.map((n) => n.id))\n      for (const key of Array.from(yNodes.keys())) {\n        if (!newIds.has(key)) yNodes.delete(key)\n      }\n      for (const node of updated) {\n        yNodes.set(node.id, JSON.stringify(node))\n      }\n    }, 'local')\n  }, [])\n\n  const syncEdgesToYjs = useCallback((updated: Edge[]) => {\n    const doc = docRef.current\n    if (!doc || !syncedRef.current) return\n\n    const yEdges = doc.getMap<string>('edges')\n    doc.transact(() => {\n      const newIds = new Set(updated.map((e) => e.id))\n      for (const key of Array.from(yEdges.keys())) {\n        if (!newIds.has(key)) yEdges.delete(key)\n      }\n      for (const edge of updated) {\n        yEdges.set(edge.id, JSON.stringify(edge))\n      }\n    }, 'local')\n  }, [])\n\n  const setNodes = useCallback(\n    (nodesOrUpdater: SetStateAction<Node[]>) => {\n      setNodesState((current) => {\n        const updated =\n          typeof nodesOrUpdater === 'function' ? nodesOrUpdater(current) : nodesOrUpdater\n        syncNodesToYjs(updated)\n        return updated\n      })\n    },\n    [syncNodesToYjs]\n  )\n\n  const setEdges = useCallback(\n    (edgesOrUpdater: SetStateAction<Edge[]>) => {\n      setEdgesState((current) => {\n        const updated =\n          typeof edgesOrUpdater === 'function' ? edgesOrUpdater(current) : edgesOrUpdater\n        syncEdgesToYjs(updated)\n        return updated\n      })\n    },\n    [syncEdgesToYjs]\n  )\n\n  const onNodesChange = useCallback((changes: NodeChange[]) => {\n    setNodesState((currentNodes) => {\n      const updated = applyNodeChanges(changes, currentNodes)\n\n      const doc = docRef.current\n      if (!doc || !syncedRef.current) return updated\n\n      const yNodes = doc.getMap<string>('nodes')\n\n      doc.transact(() => {\n        for (const change of changes) {\n          if (change.type === 'remove') {\n            yNodes.delete(change.id)\n          } else if (change.type === 'add') {\n            yNodes.set(change.item.id, JSON.stringify(change.item))\n          } else {\n            const node = updated.find((n) => n.id === change.id)\n            if (node) {\n              yNodes.set(node.id, JSON.stringify(node))\n            }\n          }\n        }\n      }, 'local')\n\n      return updated\n    })\n  }, [])\n\n  const onEdgesChange = useCallback((changes: EdgeChange[]) => {\n    setEdgesState((currentEdges) => {\n      const updated = applyEdgeChanges(changes, currentEdges)\n\n      const doc = docRef.current\n      if (!doc || !syncedRef.current) return updated\n\n      const yEdges = doc.getMap<string>('edges')\n\n      doc.transact(() => {\n        for (const change of changes) {\n          if (change.type === 'remove') {\n            yEdges.delete(change.id)\n          } else if (change.type === 'add') {\n            yEdges.set(change.item.id, JSON.stringify(change.item))\n          } else {\n            const edge = updated.find((e) => e.id === change.id)\n            if (edge) {\n              yEdges.set(edge.id, JSON.stringify(edge))\n            }\n          }\n        }\n      }, 'local')\n\n      return updated\n    })\n  }, [])\n\n  const onConnect = useCallback((connection: Connection) => {\n    setEdgesState((currentEdges) => {\n      const updated = addEdge(connection, currentEdges)\n\n      const doc = docRef.current\n      if (!doc || !syncedRef.current) return updated\n\n      const yEdges = doc.getMap<string>('edges')\n      const newEdge = updated.find((e) => !currentEdges.some((ce) => ce.id === e.id))\n\n      if (newEdge) {\n        doc.transact(() => {\n          yEdges.set(newEdge.id, JSON.stringify(newEdge))\n        }, 'local')\n      }\n\n      return updated\n    })\n  }, [])\n\n  return {\n    nodes,\n    edges,\n    synced,\n    syncError,\n    onNodesChange,\n    onEdgesChange,\n    onConnect,\n    setNodes,\n    setEdges,\n  }\n}\n",
      "type": "registry:hook"
    },
    {
      "path": "registry/default/blocks/realtime-flow/components/realtime-flow.tsx",
      "content": "'use client'\n\nimport {\n  ReactFlow,\n  ReactFlowProvider,\n  Background,\n  Controls,\n  type Node,\n  type Edge,\n  type NodeTypes,\n  type EdgeTypes,\n} from '@xyflow/react'\nimport '@xyflow/react/dist/style.css'\nimport type { SupabasePersistenceOptions } from '@supabase-labs/y-supabase'\n\nimport { RealtimeFlowOverlay } from './realtime-flow-overlay'\nimport { useRealtimeFlow } from '../hooks/use-realtime-flow'\n\ntype RealtimeFlowProps = {\n  channel: string\n  className?: string\n  style?: React.CSSProperties\n  persistence?: boolean | SupabasePersistenceOptions\n  initialNodes?: Node[]\n  initialEdges?: Edge[]\n  nodeTypes?: NodeTypes\n  edgeTypes?: EdgeTypes\n  height?: string | number\n}\n\nconst DEFAULT_HEIGHT = 550\n\nconst RealtimeFlowContent = ({\n  channel,\n  className,\n  style,\n  persistence,\n  initialNodes,\n  initialEdges,\n  nodeTypes,\n  edgeTypes,\n  height = DEFAULT_HEIGHT,\n}: RealtimeFlowProps) => {\n  const { nodes, edges, synced, syncError, onNodesChange, onEdgesChange, onConnect } =\n    useRealtimeFlow({\n      channel,\n      persistence,\n      initialNodes,\n      initialEdges,\n    })\n\n  return (\n    <div style={{ height, position: 'relative', ...style }} className={className}>\n      <ReactFlow\n        nodes={synced ? nodes : []}\n        edges={synced ? edges : []}\n        onNodesChange={synced ? onNodesChange : undefined}\n        onEdgesChange={synced ? onEdgesChange : undefined}\n        onConnect={synced ? onConnect : undefined}\n        nodeTypes={nodeTypes}\n        edgeTypes={edgeTypes}\n        fitView\n      >\n        <Background />\n        <Controls />\n      </ReactFlow>\n      {!synced && !syncError && <RealtimeFlowOverlay status=\"syncing\" />}\n      {!synced && syncError && <RealtimeFlowOverlay status=\"error\" message={syncError} />}\n    </div>\n  )\n}\n\nconst RealtimeFlow = (props: RealtimeFlowProps) => (\n  <ReactFlowProvider>\n    <RealtimeFlowContent {...props} />\n  </ReactFlowProvider>\n)\n\nexport { RealtimeFlow }\n",
      "type": "registry:component"
    },
    {
      "path": "registry/default/clients/tanstack/lib/supabase/client.ts",
      "content": "/// <reference types=\"vite/types/importMeta.d.ts\" />\nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n  return createBrowserClient(\n    import.meta.env.VITE_SUPABASE_URL!,\n    import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n  )\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "registry/default/clients/tanstack/lib/supabase/server.ts",
      "content": "import { createServerClient } from '@supabase/ssr'\nimport { getCookies, setCookie } from '@tanstack/react-start/server'\n\nexport function createClient() {\n  return createServerClient(\n    process.env.VITE_SUPABASE_URL!,\n    process.env.VITE_SUPABASE_PUBLISHABLE_KEY!,\n    {\n      cookies: {\n        getAll() {\n          return Object.entries(getCookies()).map(\n            ([name, value]) =>\n              ({\n                name,\n                value,\n              }) as { name: string; value: string }\n          )\n        },\n        setAll(cookies) {\n          cookies.forEach((cookie) => {\n            setCookie(cookie.name, cookie.value)\n          })\n        },\n      },\n    }\n  )\n}\n",
      "type": "registry:lib"
    }
  ],
  "envVars": {
    "VITE_SUPABASE_URL": "",
    "VITE_SUPABASE_PUBLISHABLE_KEY": ""
  },
  "docs": "You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.",
  "type": "registry:component"
}