{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sync-terminal",
  "title": "Sync Terminal",
  "author": "Satchmo <https://bigblocks.dev>",
  "description": "Monospace event log for blockchain sync activity with colour-coded severity levels, auto-scroll, and connection status indicator",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/new-york/blocks/sync-terminal/index.tsx",
      "content": "\"use client\"\n\nimport { useState, useCallback } from \"react\"\nimport { SyncTerminalUI } from \"./sync-terminal-ui\"\nimport {\n  useSyncTerminal,\n  type SyncEvent,\n  type SyncStatus,\n} from \"./use-sync-terminal\"\n\n// ---------------------------------------------------------------------------\n// Re-exports\n// ---------------------------------------------------------------------------\n\nexport { SyncTerminalUI, type SyncTerminalUIProps } from \"./sync-terminal-ui\"\nexport {\n  useSyncTerminal,\n  type UseSyncTerminalOptions,\n  type UseSyncTerminalReturn,\n  type SyncEvent,\n  type SyncEventLevel,\n  type SyncStatus,\n} from \"./use-sync-terminal\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Props for the composed SyncTerminal block */\nexport interface SyncTerminalProps {\n  /** Events to display in the terminal */\n  events: SyncEvent[]\n  /** Optional sync status shown in the header */\n  status?: SyncStatus\n  /** Maximum number of events to retain in the buffer (default: 200) */\n  maxEvents?: number\n  /** Header title (default: \"Sync Log\") */\n  title?: string\n  /** Show timestamps in each line (default: true) */\n  showTimestamps?: boolean\n  /** Show source labels in each line (default: true) */\n  showSource?: boolean\n  /** Whether to auto-scroll to the latest event (default: true) */\n  autoScroll?: boolean\n  /** Whether the terminal starts collapsed (default: false) */\n  defaultCollapsed?: boolean\n  /** Optional CSS class name */\n  className?: string\n}\n\n// ---------------------------------------------------------------------------\n// Composed component\n// ---------------------------------------------------------------------------\n\n/**\n * Collapsible monospace event log for blockchain sync activity with\n * colour-coded severity levels. Click the header to expand/collapse.\n *\n * Composes the `useSyncTerminal` hook with the `SyncTerminalUI`\n * presentation component.\n *\n * @example\n * ```tsx\n * import { SyncTerminal } from \"@/components/blocks/sync-terminal\"\n *\n * function Dashboard() {\n *   const [events, setEvents] = useState<SyncEvent[]>([])\n *\n *   return (\n *     <SyncTerminal\n *       events={events}\n *       status={{ blockHeight: 850123, connected: true }}\n *       defaultCollapsed={true}\n *     />\n *   )\n * }\n * ```\n */\nexport function SyncTerminal({\n  events,\n  status,\n  maxEvents = 200,\n  title = \"Sync Log\",\n  showTimestamps = true,\n  showSource = true,\n  autoScroll = true,\n  defaultCollapsed = false,\n  className,\n}: SyncTerminalProps) {\n  const [collapsed, setCollapsed] = useState(defaultCollapsed)\n  const { events: buffered, bottomRef } = useSyncTerminal({\n    events,\n    maxEvents,\n    autoScroll,\n  })\n\n  const toggleCollapse = useCallback(() => {\n    setCollapsed((prev) => !prev)\n  }, [])\n\n  return (\n    <SyncTerminalUI\n      events={buffered}\n      status={status}\n      title={title}\n      showTimestamps={showTimestamps}\n      showSource={showSource}\n      collapsed={collapsed}\n      onToggleCollapse={toggleCollapse}\n      bottomRef={bottomRef}\n      className={className}\n    />\n  )\n}\n",
      "type": "registry:block",
      "target": "~/components/blocks/sync-terminal/index.tsx"
    },
    {
      "path": "registry/new-york/blocks/sync-terminal/sync-terminal-ui.tsx",
      "content": "\"use client\"\n\nimport { useRef, useEffect, useCallback } from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport type { SyncEvent, SyncEventLevel, SyncStatus } from \"./use-sync-terminal\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface SyncTerminalUIProps {\n  /** Events to display in the terminal */\n  events: SyncEvent[]\n  /** Optional sync status shown in the header */\n  status?: SyncStatus\n  /** Header title (default: \"Sync Log\") */\n  title?: string\n  /** Show timestamps in each line (default: true) */\n  showTimestamps?: boolean\n  /** Show source labels in each line (default: true) */\n  showSource?: boolean\n  /** Whether the log area is collapsed (default: false) */\n  collapsed?: boolean\n  /** Callback when the header is clicked to toggle collapse */\n  onToggleCollapse?: () => void\n  /** Ref to attach to the scroll sentinel at the bottom */\n  bottomRef?: React.RefObject<HTMLDivElement | null>\n  /** Optional CSS class name */\n  className?: string\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nconst LEVEL_COLORS: Record<SyncEventLevel, string> = {\n  log: \"text-muted-foreground\",\n  warn: \"text-chart-4\",\n  error: \"text-destructive\",\n  success: \"text-chart-2\",\n}\n\n/** Format a unix-ms timestamp into HH:MM:SS.mmm */\nfunction formatTimestamp(ts: number): string {\n  const d = new Date(ts)\n  const h = String(d.getHours()).padStart(2, \"0\")\n  const m = String(d.getMinutes()).padStart(2, \"0\")\n  const s = String(d.getSeconds()).padStart(2, \"0\")\n  const ms = String(d.getMilliseconds()).padStart(3, \"0\")\n  return `${h}:${m}:${s}.${ms}`\n}\n\n// ---------------------------------------------------------------------------\n// UI\n// ---------------------------------------------------------------------------\n\n/**\n * Terminal-style event log display with colour-coded severity levels.\n *\n * Uses semantic theme tokens so the terminal adapts to any shadcn theme.\n * The header is clickable to collapse/expand the log area.\n *\n * @example\n * ```tsx\n * <SyncTerminalUI\n *   events={events}\n *   status={{ blockHeight: 850123, connected: true }}\n *   collapsed={false}\n *   onToggleCollapse={() => setCollapsed(c => !c)}\n * />\n * ```\n */\nexport function SyncTerminalUI({\n  events,\n  status,\n  title = \"Sync Log\",\n  showTimestamps = true,\n  showSource = true,\n  collapsed = false,\n  onToggleCollapse,\n  bottomRef,\n  className,\n}: SyncTerminalUIProps) {\n  const scrollContainerRef = useRef<HTMLDivElement | null>(null)\n\n  // Auto-scroll when events change if we have a bottomRef\n  useEffect(() => {\n    if (!collapsed && bottomRef?.current) {\n      bottomRef.current.scrollIntoView({ behavior: \"smooth\" })\n    }\n  }, [events.length, bottomRef, collapsed])\n\n  const renderStatusDot = useCallback(() => {\n    if (!status) return null\n\n    return (\n      <div className=\"flex items-center gap-2 text-xs\">\n        <span\n          className={cn(\n            \"inline-block size-2 rounded-full\",\n            status.connected ? \"bg-chart-2\" : \"bg-muted-foreground\"\n          )}\n          aria-label={status.connected ? \"Connected\" : \"Disconnected\"}\n        />\n        {status.connected && (\n          <span className=\"text-muted-foreground\">\n            #{status.blockHeight.toLocaleString()}\n          </span>\n        )}\n      </div>\n    )\n  }, [status])\n\n  return (\n    <div\n      className={cn(\n        \"flex flex-col overflow-hidden border-t border-border bg-card font-mono text-xs\",\n        className\n      )}\n    >\n      {/* Header — clickable to toggle collapse */}\n      <button\n        type=\"button\"\n        className=\"flex items-center justify-between border-b border-border px-3 py-1.5 hover:bg-accent/50 transition-colors cursor-pointer select-none\"\n        onClick={onToggleCollapse}\n        aria-expanded={!collapsed}\n        aria-controls=\"sync-terminal-log\"\n      >\n        <div className=\"flex items-center gap-2\">\n          <svg\n            className={cn(\n              \"h-3 w-3 text-muted-foreground transition-transform\",\n              collapsed ? \"-rotate-90\" : \"rotate-0\"\n            )}\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth=\"2\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n          >\n            <polyline points=\"6 9 12 15 18 9\" />\n          </svg>\n          <span className=\"font-semibold text-foreground text-xs\">{title}</span>\n          {collapsed && events.length > 0 && (\n            <span className=\"text-muted-foreground\">\n              ({events.length} events)\n            </span>\n          )}\n        </div>\n        {renderStatusDot()}\n      </button>\n\n      {/* Log area — hidden when collapsed */}\n      {!collapsed && (\n        <div\n          id=\"sync-terminal-log\"\n          ref={scrollContainerRef}\n          className=\"flex-1 overflow-y-auto p-3\"\n          style={{ maxHeight: 200 }}\n          role=\"log\"\n          aria-live=\"polite\"\n          aria-label={title}\n        >\n          {events.length === 0 ? (\n            <p className=\"text-muted-foreground\">No events yet.</p>\n          ) : (\n            <div className=\"flex flex-col gap-0.5\">\n              {events.map((event, i) => (\n                <div\n                  key={`${event.timestamp}-${i}`}\n                  className=\"flex gap-2 leading-5\"\n                >\n                  {showTimestamps && (\n                    <span className=\"shrink-0 text-muted-foreground\">\n                      {formatTimestamp(event.timestamp)}\n                    </span>\n                  )}\n                  {showSource && (\n                    <span className=\"shrink-0 text-muted-foreground\">\n                      [{event.source}]\n                    </span>\n                  )}\n                  <span className={LEVEL_COLORS[event.level]}>\n                    {event.message}\n                  </span>\n                </div>\n              ))}\n              {/* Scroll sentinel */}\n              <div ref={bottomRef} />\n            </div>\n          )}\n        </div>\n      )}\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "~/components/blocks/sync-terminal/sync-terminal-ui.tsx"
    },
    {
      "path": "registry/new-york/blocks/sync-terminal/use-sync-terminal.ts",
      "content": "import { useCallback, useEffect, useMemo, useRef, useState } from \"react\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Severity level for a sync event */\nexport type SyncEventLevel = \"log\" | \"warn\" | \"error\" | \"success\"\n\n/** A single event entry in the sync terminal */\nexport interface SyncEvent {\n  /** Unix timestamp in milliseconds */\n  timestamp: number\n  /** Origin of the event (e.g. \"sync\", \"wallet\", \"http\") */\n  source: string\n  /** Severity level */\n  level: SyncEventLevel\n  /** Human-readable message */\n  message: string\n}\n\n/** Current sync status indicator */\nexport interface SyncStatus {\n  /** Latest block height */\n  blockHeight: number\n  /** Whether the node/service is connected */\n  connected: boolean\n}\n\n/** Options for the useSyncTerminal hook */\nexport interface UseSyncTerminalOptions {\n  /** Initial events to populate the terminal with */\n  events?: SyncEvent[]\n  /** Maximum number of events to retain in the buffer (default: 200) */\n  maxEvents?: number\n  /** Whether to auto-scroll to the latest event (default: true) */\n  autoScroll?: boolean\n}\n\n/** Return value of the useSyncTerminal hook */\nexport interface UseSyncTerminalReturn {\n  /** Current buffered events (sliced to maxEvents) */\n  events: SyncEvent[]\n  /** Ref to attach to the scroll-bottom sentinel element */\n  bottomRef: React.RefObject<HTMLDivElement | null>\n  /** Append a single event to the buffer */\n  push: (event: SyncEvent) => void\n  /** Append multiple events to the buffer */\n  pushMany: (events: SyncEvent[]) => void\n  /** Clear all events */\n  clear: () => void\n  /** Whether auto-scroll is currently active */\n  isAutoScroll: boolean\n  /** Toggle auto-scroll on or off */\n  setAutoScroll: (value: boolean) => void\n}\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\n/**\n * Manages the event buffer and auto-scroll behaviour for a sync terminal.\n *\n * Events are kept in a fixed-size ring buffer (default 200). When new events\n * are pushed and `autoScroll` is enabled, the hook scrolls the sentinel ref\n * into view after each render.\n *\n * @example\n * ```ts\n * const { events, bottomRef, push, clear } = useSyncTerminal({\n *   maxEvents: 500,\n * })\n *\n * push({ timestamp: Date.now(), source: \"sync\", level: \"log\", message: \"Block 850001\" })\n * ```\n */\nexport function useSyncTerminal(\n  options: UseSyncTerminalOptions = {}\n): UseSyncTerminalReturn {\n  const {\n    events: initialEvents,\n    maxEvents = 200,\n    autoScroll: initialAutoScroll = true,\n  } = options\n\n  const [buffer, setBuffer] = useState<SyncEvent[]>(\n    () => initialEvents?.slice(-maxEvents) ?? []\n  )\n  const [isAutoScroll, setAutoScroll] = useState(initialAutoScroll)\n  const bottomRef = useRef<HTMLDivElement | null>(null)\n\n  // Scroll to bottom when buffer changes and auto-scroll is on\n  useEffect(() => {\n    if (isAutoScroll && bottomRef.current) {\n      bottomRef.current.scrollIntoView({ behavior: \"smooth\" })\n    }\n  }, [buffer, isAutoScroll])\n\n  // Sync external events prop into buffer when it changes\n  const externalKey = useMemo(\n    () => (initialEvents ? initialEvents.length : -1),\n    [initialEvents]\n  )\n\n  useEffect(() => {\n    if (initialEvents) {\n      setBuffer(initialEvents.slice(-maxEvents))\n    }\n  }, [externalKey, maxEvents])\n\n  const push = useCallback(\n    (event: SyncEvent) => {\n      setBuffer((prev) => {\n        const next = [...prev, event]\n        return next.length > maxEvents ? next.slice(-maxEvents) : next\n      })\n    },\n    [maxEvents]\n  )\n\n  const pushMany = useCallback(\n    (events: SyncEvent[]) => {\n      setBuffer((prev) => {\n        const next = [...prev, ...events]\n        return next.length > maxEvents ? next.slice(-maxEvents) : next\n      })\n    },\n    [maxEvents]\n  )\n\n  const clear = useCallback(() => {\n    setBuffer([])\n  }, [])\n\n  return {\n    events: buffer,\n    bottomRef,\n    push,\n    pushMany,\n    clear,\n    isAutoScroll,\n    setAutoScroll,\n  }\n}\n",
      "type": "registry:component",
      "target": "~/components/blocks/sync-terminal/use-sync-terminal.ts"
    }
  ],
  "categories": [
    "wallet"
  ],
  "type": "registry:block"
}