{
  "name": "developer-tools-syncassetscomponent",
  "type": "registry:component",
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/developer-tools/SyncAssetsComponent.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport {\n\tCheckCircledIcon,\n\tClockIcon,\n\tCrossCircledIcon,\n\tExternalLinkIcon,\n\tFileIcon,\n\tInfoCircledIcon,\n\tTrashIcon,\n\tUpdateIcon,\n} from \"@radix-ui/react-icons\";\nimport { useCallback, useState } from \"react\";\nimport { cn } from \"../../lib/utils.js\";\nimport { DropZone } from \"../ui-components/DropZone.js\";\nimport { ErrorDisplay } from \"../ui-components/ErrorDisplay.js\";\nimport { LoadingButton } from \"../ui-components/LoadingButton.js\";\nimport { Badge } from \"../ui/badge.js\";\nimport { Button } from \"../ui/button.js\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"../ui/card.js\";\nimport { Progress } from \"../ui/progress.js\";\nimport {\n\tTable,\n\tTableBody,\n\tTableCell,\n\tTableHead,\n\tTableHeader,\n\tTableRow,\n} from \"../ui/table.js\";\nimport {\n\tTooltip,\n\tTooltipContent,\n\tTooltipProvider,\n\tTooltipTrigger,\n} from \"../ui/tooltip.js\";\n\n// File sync status enum\nexport enum FileSyncStatus {\n\tPending = \"pending\",\n\tChecking = \"checking\",\n\tOnChain = \"on-chain\",\n\tNeedsSync = \"needs-sync\",\n\tSyncing = \"syncing\",\n\tSynced = \"synced\",\n\tFailed = \"failed\",\n}\n\n// Sync file interface\nexport interface SyncFile {\n\tid: string;\n\tfile: File;\n\thash?: string;\n\tstatus: FileSyncStatus;\n\tsize: number;\n\ttxid?: string;\n\terror?: string;\n\tprogress?: number;\n\testimatedFee?: number;\n}\n\n// Sync summary interface\nexport interface SyncSummary {\n\ttotalFiles: number;\n\tneedsSyncCount: number;\n\ttotalSize: number; // in bytes\n\testimatedTotalFee: number; // in satoshis\n}\n\n// Component props\nexport interface SyncAssetsComponentProps {\n\tonSyncComplete?: (results: {\n\t\tsuccess: SyncFile[];\n\t\tfailed: SyncFile[];\n\t}) => void;\n\tonError?: (error: string) => void;\n\tmaxFileSize?: number; // Max file size in bytes (default 10MB)\n\tmaxFiles?: number; // Max number of files (default 50)\n\tacceptedTypes?: string[]; // Accepted file types\n\testimatedFeePerKB?: number; // Estimated fee per KB in satoshis (default 500)\n\tclassName?: string;\n}\n\n// Calculate file hash (using Web Crypto API)\nasync function calculateFileHash(file: File): Promise<string> {\n\tconst arrayBuffer = await file.arrayBuffer();\n\tconst hashBuffer = await crypto.subtle.digest(\"SHA-256\", arrayBuffer);\n\tconst hashArray = Array.from(new Uint8Array(hashBuffer));\n\treturn hashArray.map((b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\n// Check if file exists on blockchain via ordfs API\nasync function checkFileOnChain(\n\thash: string,\n): Promise<{ exists: boolean; txid?: string }> {\n\ttry {\n\t\t// Check ordfs.network API for file existence\n\t\tconst response = await fetch(`https://ordfs.network/api/files/${hash}`, {\n\t\t\tmethod: \"GET\",\n\t\t\theaders: {\n\t\t\t\tAccept: \"application/json\",\n\t\t\t},\n\t\t});\n\n\t\tif (response.ok) {\n\t\t\tconst data = await response.json();\n\t\t\treturn {\n\t\t\t\texists: true,\n\t\t\t\ttxid: data.txid || data.outpoint?.split(\"_\")[0],\n\t\t\t};\n\t\t}\n\n\t\tif (response.status === 404) {\n\t\t\treturn { exists: false };\n\t\t}\n\n\t\tthrow new Error(`API error: ${response.status}`);\n\t} catch (error) {\n\t\tconsole.warn(\"Error checking file on chain:\", error);\n\t\t// Fallback: assume file doesn't exist if API check fails\n\t\treturn { exists: false };\n\t}\n}\n\n// Estimate transaction fee based on file size\nfunction estimateTransactionFee(fileSizeBytes: number, feePerKB = 500): number {\n\t// Basic estimation: base tx size + file data\n\tconst baseTxSize = 250; // bytes\n\tconst totalSize = baseTxSize + fileSizeBytes;\n\tconst sizeInKB = totalSize / 1024;\n\treturn Math.ceil(sizeInKB * feePerKB);\n}\n\n// Format file size for display\nfunction formatFileSize(bytes: number): string {\n\tif (bytes === 0) return \"0 B\";\n\tconst k = 1024;\n\tconst sizes = [\"B\", \"KB\", \"MB\", \"GB\"];\n\tconst i = Math.floor(Math.log(bytes) / Math.log(k));\n\treturn `${(bytes / k ** i).toFixed(1)} ${sizes[i]}`;\n}\n\n// Format satoshis for display\nfunction formatSatoshis(satoshis: number): string {\n\tif (satoshis >= 100000000) {\n\t\treturn `${(satoshis / 100000000).toFixed(4)} BSV`;\n\t}\n\treturn `${satoshis.toLocaleString()} sats`;\n}\n\nexport function SyncAssetsComponent({\n\tonSyncComplete,\n\tonError,\n\tmaxFileSize = 10 * 1024 * 1024, // 10MB\n\tmaxFiles = 50,\n\tacceptedTypes = [\n\t\t\".jpg\",\n\t\t\".jpeg\",\n\t\t\".png\",\n\t\t\".gif\",\n\t\t\".webp\",\n\t\t\".svg\",\n\t\t\".pdf\",\n\t\t\".txt\",\n\t\t\".json\",\n\t\t\".md\",\n\t\t\".html\",\n\t],\n\testimatedFeePerKB = 500,\n\tclassName = \"\",\n}: SyncAssetsComponentProps) {\n\tconst [files, setFiles] = useState<SyncFile[]>([]);\n\tconst [isChecking, setIsChecking] = useState(false);\n\tconst [isSyncing, setIsSyncing] = useState(false);\n\tconst [error, setError] = useState<string | null>(null);\n\tconst [syncSummary, setSyncSummary] = useState<SyncSummary | null>(null);\n\n\t// Handle file drop\n\tconst handleFiles = useCallback(\n\t\tasync (fileList: FileList) => {\n\t\t\tsetError(null);\n\t\t\tconst newFiles = Array.from(fileList);\n\n\t\t\t// Validate file count\n\t\t\tif (files.length + newFiles.length > maxFiles) {\n\t\t\t\tsetError(\n\t\t\t\t\t`Cannot add more than ${maxFiles} files. Remove some files first.`,\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Validate and create SyncFile objects\n\t\t\tconst validFiles: SyncFile[] = [];\n\t\t\tfor (const file of newFiles) {\n\t\t\t\t// Check file size\n\t\t\t\tif (file.size > maxFileSize) {\n\t\t\t\t\tsetError(\n\t\t\t\t\t\t`File \"${file.name}\" is too large. Maximum size is ${formatFileSize(maxFileSize)}.`,\n\t\t\t\t\t);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Check file type\n\t\t\t\tconst fileExtension = `.${file.name.split(\".\").pop()?.toLowerCase()}`;\n\t\t\t\tif (!acceptedTypes.includes(fileExtension)) {\n\t\t\t\t\tsetError(`File type \"${fileExtension}\" is not supported.`);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Check for duplicates\n\t\t\t\tconst isDuplicate = files.some(\n\t\t\t\t\t(f) => f.file.name === file.name && f.file.size === file.size,\n\t\t\t\t);\n\t\t\t\tif (isDuplicate) {\n\t\t\t\t\tsetError(`File \"${file.name}\" is already in the list.`);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tvalidFiles.push({\n\t\t\t\t\tid: crypto.randomUUID(),\n\t\t\t\t\tfile,\n\t\t\t\t\tstatus: FileSyncStatus.Pending,\n\t\t\t\t\tsize: file.size,\n\t\t\t\t\testimatedFee: estimateTransactionFee(file.size, estimatedFeePerKB),\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (validFiles.length > 0) {\n\t\t\t\tsetFiles((prev) => [...prev, ...validFiles]);\n\t\t\t}\n\t\t},\n\t\t[files, maxFiles, maxFileSize, acceptedTypes, estimatedFeePerKB],\n\t);\n\n\t// Remove file from list\n\tconst removeFile = useCallback((fileId: string) => {\n\t\tsetFiles((prev) => prev.filter((f) => f.id !== fileId));\n\t\tsetError(null);\n\t}, []);\n\n\t// Clear all files\n\tconst clearAllFiles = useCallback(() => {\n\t\tsetFiles([]);\n\t\tsetError(null);\n\t\tsetSyncSummary(null);\n\t}, []);\n\n\t// Check files on blockchain\n\tconst checkFiles = useCallback(async () => {\n\t\tif (files.length === 0) return;\n\n\t\tsetIsChecking(true);\n\t\tsetError(null);\n\n\t\ttry {\n\t\t\tconst updatedFiles: SyncFile[] = [];\n\n\t\t\tfor (const file of files) {\n\t\t\t\t// Skip files that are already checked\n\t\t\t\tif (file.status !== FileSyncStatus.Pending) {\n\t\t\t\t\tupdatedFiles.push(file);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Update status to checking\n\t\t\t\tconst checkingFile = { ...file, status: FileSyncStatus.Checking };\n\t\t\t\tsetFiles((prev) =>\n\t\t\t\t\tprev.map((f) => (f.id === file.id ? checkingFile : f)),\n\t\t\t\t);\n\n\t\t\t\ttry {\n\t\t\t\t\t// Calculate file hash\n\t\t\t\t\tconst hash = await calculateFileHash(file.file);\n\n\t\t\t\t\t// Check if file exists on blockchain\n\t\t\t\t\tconst { exists, txid } = await checkFileOnChain(hash);\n\n\t\t\t\t\tconst updatedFile = {\n\t\t\t\t\t\t...checkingFile,\n\t\t\t\t\t\thash,\n\t\t\t\t\t\tstatus: exists ? FileSyncStatus.OnChain : FileSyncStatus.NeedsSync,\n\t\t\t\t\t\ttxid: exists ? txid : undefined,\n\t\t\t\t\t};\n\n\t\t\t\t\tupdatedFiles.push(updatedFile);\n\t\t\t\t\tsetFiles((prev) =>\n\t\t\t\t\t\tprev.map((f) => (f.id === file.id ? updatedFile : f)),\n\t\t\t\t\t);\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst failedFile = {\n\t\t\t\t\t\t...checkingFile,\n\t\t\t\t\t\tstatus: FileSyncStatus.Failed,\n\t\t\t\t\t\terror:\n\t\t\t\t\t\t\terr instanceof Error ? err.message : \"Failed to process file\",\n\t\t\t\t\t};\n\t\t\t\t\tupdatedFiles.push(failedFile);\n\t\t\t\t\tsetFiles((prev) =>\n\t\t\t\t\t\tprev.map((f) => (f.id === file.id ? failedFile : f)),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Calculate sync summary\n\t\t\tconst needsSyncFiles = updatedFiles.filter(\n\t\t\t\t(f) => f.status === FileSyncStatus.NeedsSync,\n\t\t\t);\n\t\t\tconst summary: SyncSummary = {\n\t\t\t\ttotalFiles: needsSyncFiles.length,\n\t\t\t\tneedsSyncCount: needsSyncFiles.length,\n\t\t\t\ttotalSize: needsSyncFiles.reduce((sum, f) => sum + f.size, 0),\n\t\t\t\testimatedTotalFee: needsSyncFiles.reduce(\n\t\t\t\t\t(sum, f) => sum + (f.estimatedFee || 0),\n\t\t\t\t\t0,\n\t\t\t\t),\n\t\t\t};\n\n\t\t\tsetSyncSummary(summary);\n\t\t} catch (err) {\n\t\t\tconst errorMessage =\n\t\t\t\terr instanceof Error ? err.message : \"Failed to check files\";\n\t\t\tsetError(errorMessage);\n\t\t\tonError?.(errorMessage);\n\t\t} finally {\n\t\t\tsetIsChecking(false);\n\t\t}\n\t}, [files, onError]);\n\n\t// Sync files to blockchain\n\tconst syncFiles = useCallback(async () => {\n\t\tconst filesToSync = files.filter(\n\t\t\t(f) => f.status === FileSyncStatus.NeedsSync,\n\t\t);\n\t\tif (filesToSync.length === 0) return;\n\n\t\tsetIsSyncing(true);\n\t\tsetError(null);\n\n\t\tconst results = { success: [] as SyncFile[], failed: [] as SyncFile[] };\n\n\t\ttry {\n\t\t\tfor (let i = 0; i < filesToSync.length; i++) {\n\t\t\t\tconst file = filesToSync[i];\n\t\t\t\tif (!file) continue; // TypeScript guard\n\n\t\t\t\t// Update status to syncing\n\t\t\t\tsetFiles((prev) =>\n\t\t\t\t\tprev.map((f) =>\n\t\t\t\t\t\tf.id === file.id\n\t\t\t\t\t\t\t? { ...f, status: FileSyncStatus.Syncing, progress: 0 }\n\t\t\t\t\t\t\t: f,\n\t\t\t\t\t),\n\t\t\t\t);\n\n\t\t\t\ttry {\n\t\t\t\t\t// Simulate blockchain sync progress\n\t\t\t\t\tfor (let progress = 0; progress <= 100; progress += 10) {\n\t\t\t\t\t\tsetFiles((prev) =>\n\t\t\t\t\t\t\tprev.map((f) => (f.id === file.id ? { ...f, progress } : f)),\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\t// Simulate network delay\n\t\t\t\t\t\tawait new Promise((resolve) => setTimeout(resolve, 200));\n\t\t\t\t\t}\n\n\t\t\t\t\t// TODO: Implement actual blockchain sync\n\t\t\t\t\t// This would involve:\n\t\t\t\t\t// 1. Creating a transaction with the file data\n\t\t\t\t\t// 2. Broadcasting it to the network\n\t\t\t\t\t// 3. Getting the transaction ID\n\n\t\t\t\t\t// For now, simulate successful sync\n\t\t\t\t\tconst mockTxid = crypto\n\t\t\t\t\t\t.randomUUID()\n\t\t\t\t\t\t.replace(/-/g, \"\")\n\t\t\t\t\t\t.substring(0, 64);\n\n\t\t\t\t\tconst syncedFile: SyncFile = {\n\t\t\t\t\t\t...file,\n\t\t\t\t\t\tstatus: FileSyncStatus.Synced,\n\t\t\t\t\t\ttxid: mockTxid,\n\t\t\t\t\t\tprogress: 100,\n\t\t\t\t\t};\n\n\t\t\t\t\tsetFiles((prev) =>\n\t\t\t\t\t\tprev.map((f) => (f.id === file.id ? syncedFile : f)),\n\t\t\t\t\t);\n\t\t\t\t\tresults.success.push(syncedFile);\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst failedFile: SyncFile = {\n\t\t\t\t\t\t...file,\n\t\t\t\t\t\tstatus: FileSyncStatus.Failed,\n\t\t\t\t\t\terror: err instanceof Error ? err.message : \"Failed to sync file\",\n\t\t\t\t\t};\n\n\t\t\t\t\tsetFiles((prev) =>\n\t\t\t\t\t\tprev.map((f) => (f.id === file.id ? failedFile : f)),\n\t\t\t\t\t);\n\t\t\t\t\tresults.failed.push(failedFile);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tonSyncComplete?.(results);\n\t\t} catch (err) {\n\t\t\tconst errorMessage =\n\t\t\t\terr instanceof Error ? err.message : \"Failed to sync files\";\n\t\t\tsetError(errorMessage);\n\t\t\tonError?.(errorMessage);\n\t\t} finally {\n\t\t\tsetIsSyncing(false);\n\t\t}\n\t}, [files, onSyncComplete, onError]);\n\n\t// Get status icon\n\tconst getStatusIcon = (status: FileSyncStatus) => {\n\t\tswitch (status) {\n\t\t\tcase FileSyncStatus.Pending:\n\t\t\t\treturn (\n\t\t\t\t\t<ClockIcon\n\t\t\t\t\t\twidth=\"16\"\n\t\t\t\t\t\theight=\"16\"\n\t\t\t\t\t\tstyle={{ color: \"var(--muted-foreground)\" }}\n\t\t\t\t\t/>\n\t\t\t\t);\n\t\t\tcase FileSyncStatus.Checking:\n\t\t\t\treturn (\n\t\t\t\t\t<UpdateIcon\n\t\t\t\t\t\twidth=\"16\"\n\t\t\t\t\t\theight=\"16\"\n\t\t\t\t\t\tclassName=\"animate-spin\"\n\t\t\t\t\t\tstyle={{ color: \"var(--primary-foreground)\" }}\n\t\t\t\t\t/>\n\t\t\t\t);\n\t\t\tcase FileSyncStatus.OnChain:\n\t\t\t\treturn (\n\t\t\t\t\t<CheckCircledIcon\n\t\t\t\t\t\twidth=\"16\"\n\t\t\t\t\t\theight=\"16\"\n\t\t\t\t\t\tstyle={{ color: \"var(--primary-foreground)\" }}\n\t\t\t\t\t/>\n\t\t\t\t);\n\t\t\tcase FileSyncStatus.NeedsSync:\n\t\t\t\treturn (\n\t\t\t\t\t<InfoCircledIcon\n\t\t\t\t\t\twidth=\"16\"\n\t\t\t\t\t\theight=\"16\"\n\t\t\t\t\t\tstyle={{ color: \"var(--destructive-foreground)\" }}\n\t\t\t\t\t/>\n\t\t\t\t);\n\t\t\tcase FileSyncStatus.Syncing:\n\t\t\t\treturn (\n\t\t\t\t\t<UpdateIcon\n\t\t\t\t\t\twidth=\"16\"\n\t\t\t\t\t\theight=\"16\"\n\t\t\t\t\t\tclassName=\"animate-spin\"\n\t\t\t\t\t\tstyle={{ color: \"var(--primary-foreground)\" }}\n\t\t\t\t\t/>\n\t\t\t\t);\n\t\t\tcase FileSyncStatus.Synced:\n\t\t\t\treturn (\n\t\t\t\t\t<CheckCircledIcon\n\t\t\t\t\t\twidth=\"16\"\n\t\t\t\t\t\theight=\"16\"\n\t\t\t\t\t\tstyle={{ color: \"var(--primary-foreground)\" }}\n\t\t\t\t\t/>\n\t\t\t\t);\n\t\t\tcase FileSyncStatus.Failed:\n\t\t\t\treturn (\n\t\t\t\t\t<CrossCircledIcon\n\t\t\t\t\t\twidth=\"16\"\n\t\t\t\t\t\theight=\"16\"\n\t\t\t\t\t\tstyle={{ color: \"var(--destructive-foreground)\" }}\n\t\t\t\t\t/>\n\t\t\t\t);\n\t\t}\n\t};\n\n\t// Get status color\n\tconst getStatusColor = (status: FileSyncStatus) => {\n\t\tswitch (status) {\n\t\t\tcase FileSyncStatus.Pending:\n\t\t\t\treturn \"gray\";\n\t\t\tcase FileSyncStatus.Checking:\n\t\t\tcase FileSyncStatus.Syncing:\n\t\t\t\treturn \"blue\";\n\t\t\tcase FileSyncStatus.OnChain:\n\t\t\tcase FileSyncStatus.Synced:\n\t\t\t\treturn \"green\";\n\t\t\tcase FileSyncStatus.NeedsSync:\n\t\t\t\treturn \"amber\";\n\t\t\tcase FileSyncStatus.Failed:\n\t\t\t\treturn \"red\";\n\t\t}\n\t};\n\n\t// Get status text\n\tconst getStatusText = (file: SyncFile) => {\n\t\tswitch (file.status) {\n\t\t\tcase FileSyncStatus.Pending:\n\t\t\t\treturn \"Pending\";\n\t\t\tcase FileSyncStatus.Checking:\n\t\t\t\treturn \"Checking...\";\n\t\t\tcase FileSyncStatus.OnChain:\n\t\t\t\treturn \"On Chain\";\n\t\t\tcase FileSyncStatus.NeedsSync:\n\t\t\t\treturn \"Needs Sync\";\n\t\t\tcase FileSyncStatus.Syncing:\n\t\t\t\treturn `Syncing... ${file.progress || 0}%`;\n\t\t\tcase FileSyncStatus.Synced:\n\t\t\t\treturn \"Synced\";\n\t\t\tcase FileSyncStatus.Failed:\n\t\t\t\treturn file.error || \"Failed\";\n\t\t}\n\t};\n\n\tconst hasFiles = files.length > 0;\n\tconst hasPendingFiles = files.some(\n\t\t(f) => f.status === FileSyncStatus.Pending,\n\t);\n\tconst hasFilesToSync = files.some(\n\t\t(f) => f.status === FileSyncStatus.NeedsSync,\n\t);\n\tconst isProcessing = isChecking || isSyncing;\n\n\treturn (\n\t\t<div className={cn(\"flex flex-col gap-6\", className)}>\n\t\t\t{/* Drop Zone */}\n\t\t\t<DropZone\n\t\t\t\tmultiple\n\t\t\t\taccept={acceptedTypes.join(\",\")}\n\t\t\t\tmaxSize={maxFileSize}\n\t\t\t\tdisabled={isProcessing}\n\t\t\t\ticon={<FileIcon />}\n\t\t\t\ttitle=\"Sync Files to Blockchain\"\n\t\t\t\tdescription={`Drop files here or click to browse (max ${maxFiles} files, ${formatFileSize(maxFileSize)} each)`}\n\t\t\t\tdragActiveText=\"Drop files here\"\n\t\t\t\tdragActiveDescription=\"Release to add files\"\n\t\t\t\theight={150}\n\t\t\t\tonDrop={handleFiles}\n\t\t\t\tonError={setError}\n\t\t\t/>\n\n\t\t\t{/* Error Display */}\n\t\t\t{error && <ErrorDisplay error={error} />}\n\n\t\t\t{/* File List */}\n\t\t\t{hasFiles && (\n\t\t\t\t<Card>\n\t\t\t\t\t<CardContent className=\"p-6\">\n\t\t\t\t\t\t<div className=\"flex flex-col gap-4\">\n\t\t\t\t\t\t\t{/* File List Header */}\n\t\t\t\t\t\t\t<div className=\"flex items-center justify-between\">\n\t\t\t\t\t\t\t\t<p className=\"text-lg font-medium\">Files ({files.length})</p>\n\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\t\t\t\tonClick={clearAllFiles}\n\t\t\t\t\t\t\t\t\tdisabled={isProcessing}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\tClear All\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t{/* Files Table */}\n\t\t\t\t\t\t\t<Table>\n\t\t\t\t\t\t\t\t<TableHeader>\n\t\t\t\t\t\t\t\t\t<TableRow>\n\t\t\t\t\t\t\t\t\t\t<TableHead>File</TableHead>\n\t\t\t\t\t\t\t\t\t\t<TableHead>Size</TableHead>\n\t\t\t\t\t\t\t\t\t\t<TableHead>Status</TableHead>\n\t\t\t\t\t\t\t\t\t\t<TableHead>Fee</TableHead>\n\t\t\t\t\t\t\t\t\t\t<TableHead>Action</TableHead>\n\t\t\t\t\t\t\t\t\t</TableRow>\n\t\t\t\t\t\t\t\t</TableHeader>\n\t\t\t\t\t\t\t\t<TableBody>\n\t\t\t\t\t\t\t\t\t{files.map((file) => (\n\t\t\t\t\t\t\t\t\t\t<TableRow key={file.id}>\n\t\t\t\t\t\t\t\t\t\t\t<TableCell>\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"flex items-center gap-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<FileIcon width=\"16\" height=\"16\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-sm font-medium\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{file.file.name}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{file.hash && (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-xs text-muted-foreground font-mono\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{file.hash.substring(0, 16)}...\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t</TableCell>\n\t\t\t\t\t\t\t\t\t\t\t<TableCell>\n\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-sm\">{formatFileSize(file.size)}</p>\n\t\t\t\t\t\t\t\t\t\t\t</TableCell>\n\t\t\t\t\t\t\t\t\t\t\t<TableCell>\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"flex items-center gap-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{getStatusIcon(file.status)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Badge variant=\"secondary\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{getStatusText(file)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t{file.status === FileSyncStatus.Syncing &&\n\t\t\t\t\t\t\t\t\t\t\t\t\tfile.progress !== undefined && (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"mt-1 w-20\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Progress value={file.progress} className=\"h-1\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t</TableCell>\n\t\t\t\t\t\t\t\t\t\t\t<TableCell>\n\t\t\t\t\t\t\t\t\t\t\t\t{file.estimatedFee && (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<p className=\"text-sm\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{formatSatoshis(file.estimatedFee)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t</TableCell>\n\t\t\t\t\t\t\t\t\t\t\t<TableCell>\n\t\t\t\t\t\t\t\t\t\t\t\t<TooltipProvider>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"flex items-center gap-1\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{file.txid && (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Tooltip>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<TooltipTrigger asChild>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tonClick={() =>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\twindow.open(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t`https://whatsonchain.com/tx/${file.txid}`,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"_blank\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<ExternalLinkIcon className=\"h-3.5 w-3.5\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</TooltipTrigger>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<TooltipContent>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tView on blockchain\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</TooltipContent>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Tooltip>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Tooltip>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<TooltipTrigger asChild>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tonClick={() => removeFile(file.id)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tdisabled={isProcessing}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"text-destructive hover:text-destructive\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<TrashIcon className=\"h-3.5 w-3.5\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</TooltipTrigger>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<TooltipContent>Remove file</TooltipContent>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Tooltip>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t</TooltipProvider>\n\t\t\t\t\t\t\t\t\t\t\t</TableCell>\n\t\t\t\t\t\t\t\t\t\t</TableRow>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</TableBody>\n\t\t\t\t\t\t\t</Table>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</CardContent>\n\t\t\t\t</Card>\n\t\t\t)}\n\n\t\t\t{/* Action Buttons */}\n\t\t\t{hasFiles && (\n\t\t\t\t<div className=\"flex gap-3 justify-end\">\n\t\t\t\t\t<LoadingButton\n\t\t\t\t\t\tonClick={checkFiles}\n\t\t\t\t\t\tloading={isChecking}\n\t\t\t\t\t\tdisabled={!hasPendingFiles || isSyncing}\n\t\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\t>\n\t\t\t\t\t\tCheck Blockchain Status\n\t\t\t\t\t</LoadingButton>\n\n\t\t\t\t\t{syncSummary && (\n\t\t\t\t\t\t<LoadingButton\n\t\t\t\t\t\t\tonClick={syncFiles}\n\t\t\t\t\t\t\tloading={isSyncing}\n\t\t\t\t\t\t\tdisabled={!hasFilesToSync || isChecking}\n\t\t\t\t\t\t\tvariant=\"default\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\tSync {syncSummary.needsSyncCount} File\n\t\t\t\t\t\t\t{syncSummary.needsSyncCount !== 1 ? \"s\" : \"\"}\n\t\t\t\t\t\t</LoadingButton>\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t)}\n\n\t\t\t{/* Sync Summary */}\n\t\t\t{syncSummary && syncSummary.needsSyncCount > 0 && (\n\t\t\t\t<Card>\n\t\t\t\t\t<CardHeader>\n\t\t\t\t\t\t<CardTitle className=\"text-base\">Sync Summary</CardTitle>\n\t\t\t\t\t</CardHeader>\n\t\t\t\t\t<CardContent className=\"space-y-3\">\n\t\t\t\t\t\t<div className=\"flex justify-between\">\n\t\t\t\t\t\t\t<span className=\"text-sm text-muted-foreground\">\n\t\t\t\t\t\t\t\tFiles to Sync\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<span className=\"text-sm font-medium\">\n\t\t\t\t\t\t\t\t{syncSummary.needsSyncCount}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div className=\"flex justify-between\">\n\t\t\t\t\t\t\t<span className=\"text-sm text-muted-foreground\">Total Size</span>\n\t\t\t\t\t\t\t<span className=\"text-sm font-medium\">\n\t\t\t\t\t\t\t\t{formatFileSize(syncSummary.totalSize)}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div className=\"flex justify-between\">\n\t\t\t\t\t\t\t<span className=\"text-sm text-muted-foreground\">\n\t\t\t\t\t\t\t\tEstimated Fee\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<span className=\"text-sm font-medium\">\n\t\t\t\t\t\t\t\t{formatSatoshis(syncSummary.estimatedTotalFee)}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</CardContent>\n\t\t\t\t</Card>\n\t\t\t)}\n\t\t</div>\n\t);\n}\n",
      "target": "<%- config.aliases.components %>/syncassetscomponent.tsx"
    }
  ]
}