{
  "name": "backup-recovery-fileimport",
  "type": "registry:component",
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/backup-recovery/FileImport.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { useCallback, useState } from \"react\";\nimport {\n\tDEFAULT_ALLOWED_BACKUP_TYPES,\n\tcreateUnsupportedBackupError,\n\tdetectBackupType,\n\tisBackupTypeAllowed,\n} from \"../../lib/backup-utils.js\";\nimport type { BackupTypeName } from \"../../lib/types.js\";\nimport { cn } from \"../../lib/utils.js\";\nimport { DropZone, ErrorDisplay } from \"../ui-components/index.js\";\nimport { Card, CardContent } from \"../ui/card.js\";\n\nexport interface FileValidationResult {\n\tisValid: boolean;\n\tfileType: \"encrypted\" | \"unencrypted\" | \"unknown\";\n\tformat: \"json\" | \"text\" | \"binary\" | \"unknown\";\n\terror?: string;\n\tdata?: Record<string, unknown> | string;\n}\n\nexport interface FileImportProps {\n\tonFileSelect?: (file: File) => void;\n\tonFileValidated?: (file: File, result: FileValidationResult) => void;\n\tonError?: (error: string) => void;\n\taccept?: string;\n\tdisabled?: boolean;\n\tloading?: boolean;\n\tmaxSize?: number; // in bytes\n\tvalidateFile?: (\n\t\tcontent: string,\n\t\tfile: File,\n\t) => Promise<FileValidationResult> | FileValidationResult;\n\tclassName?: string;\n\t// Backup type configuration\n\tallowedBackupTypes?: BackupTypeName[];\n\tunsupportedBackupMessage?: string;\n}\n\nconst DEFAULT_MAX_SIZE = 10 * 1024 * 1024; // 10MB\n\nexport function FileImport({\n\tonFileSelect,\n\tonFileValidated,\n\tonError,\n\taccept = \".json,.txt\",\n\tdisabled = false,\n\tloading = false,\n\tmaxSize = DEFAULT_MAX_SIZE,\n\tvalidateFile,\n\tclassName = \"\",\n\tallowedBackupTypes = DEFAULT_ALLOWED_BACKUP_TYPES,\n\tunsupportedBackupMessage,\n}: FileImportProps) {\n\tconst [processing, setProcessing] = useState(false);\n\tconst [error, setError] = useState(\"\");\n\n\tconst defaultValidateFile = useCallback(\n\t\t(content: string, _file: File): FileValidationResult => {\n\t\t\ttry {\n\t\t\t\t// First, try to parse as JSON\n\t\t\t\tlet parsedData: Record<string, unknown> | string;\n\t\t\t\tlet isJson = false;\n\n\t\t\t\ttry {\n\t\t\t\t\tparsedData = JSON.parse(content);\n\t\t\t\t\tisJson = true;\n\t\t\t\t} catch {\n\t\t\t\t\t// Not JSON, treat as text\n\t\t\t\t\tparsedData = content.trim();\n\t\t\t\t}\n\n\t\t\t\tif (isJson && typeof parsedData === \"object\" && parsedData !== null) {\n\t\t\t\t\t// Check for encrypted backup structure\n\t\t\t\t\tif (\"iv\" in parsedData && \"data\" in parsedData) {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tisValid: true,\n\t\t\t\t\t\t\tfileType: \"encrypted\",\n\t\t\t\t\t\t\tformat: \"json\",\n\t\t\t\t\t\t\tdata: parsedData,\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\n\t\t\t\t\t// Check for legacy encrypted format\n\t\t\t\t\tif (\"encrypted\" in parsedData && \"encryptedMnemonic\" in parsedData) {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tisValid: true,\n\t\t\t\t\t\t\tfileType: \"encrypted\",\n\t\t\t\t\t\t\tformat: \"json\",\n\t\t\t\t\t\t\tdata: parsedData,\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\n\t\t\t\t\t// Check for unencrypted backup structure and validate backup type\n\t\t\t\t\tconst backupType = detectBackupType(parsedData);\n\t\t\t\t\tif (backupType) {\n\t\t\t\t\t\tif (isBackupTypeAllowed(backupType, allowedBackupTypes)) {\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tisValid: true,\n\t\t\t\t\t\t\t\tfileType: \"unencrypted\",\n\t\t\t\t\t\t\t\tformat: \"json\",\n\t\t\t\t\t\t\t\tdata: parsedData,\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Valid backup format but not allowed\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tisValid: false,\n\t\t\t\t\t\t\tfileType: \"unencrypted\",\n\t\t\t\t\t\t\tformat: \"json\",\n\t\t\t\t\t\t\terror: createUnsupportedBackupError(\n\t\t\t\t\t\t\t\tbackupType,\n\t\t\t\t\t\t\t\tallowedBackupTypes,\n\t\t\t\t\t\t\t\tunsupportedBackupMessage,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tdata: parsedData,\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\n\t\t\t\t\t// JSON but unknown structure\n\t\t\t\t\treturn {\n\t\t\t\t\t\tisValid: false,\n\t\t\t\t\t\tfileType: \"unknown\",\n\t\t\t\t\t\tformat: \"json\",\n\t\t\t\t\t\terror: \"JSON file does not contain a recognized backup structure\",\n\t\t\t\t\t};\n\t\t\t\t}\n\n\t\t\t\t// Check if it's a plain encrypted string (base64-like)\n\t\t\t\tif (typeof parsedData === \"string\" && parsedData.length > 100) {\n\t\t\t\t\tconst trimmed = parsedData.trim();\n\n\t\t\t\t\t// Check for base64-like encrypted string\n\t\t\t\t\tif (/^[A-Za-z0-9+/=]+$/.test(trimmed) && trimmed.length % 4 === 0) {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tisValid: true,\n\t\t\t\t\t\t\tfileType: \"encrypted\",\n\t\t\t\t\t\t\tformat: \"text\",\n\t\t\t\t\t\t\tdata: trimmed,\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\n\t\t\t\t\t// Check for other encrypted formats\n\t\t\t\t\tif (trimmed.includes(\"-----BEGIN\") && trimmed.includes(\"-----END\")) {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tisValid: true,\n\t\t\t\t\t\t\tfileType: \"encrypted\",\n\t\t\t\t\t\t\tformat: \"text\",\n\t\t\t\t\t\t\tdata: trimmed,\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Unknown format\n\t\t\t\treturn {\n\t\t\t\t\tisValid: false,\n\t\t\t\t\tfileType: \"unknown\",\n\t\t\t\t\tformat: isJson ? \"json\" : \"text\",\n\t\t\t\t\terror: \"File does not contain a recognized backup format\",\n\t\t\t\t};\n\t\t\t} catch (err) {\n\t\t\t\treturn {\n\t\t\t\t\tisValid: false,\n\t\t\t\t\tfileType: \"unknown\",\n\t\t\t\t\tformat: \"unknown\",\n\t\t\t\t\terror:\n\t\t\t\t\t\terr instanceof Error ? err.message : \"Failed to parse file content\",\n\t\t\t\t};\n\t\t\t}\n\t\t},\n\t\t[allowedBackupTypes, unsupportedBackupMessage],\n\t);\n\n\tconst processFile = useCallback(\n\t\tasync (file: File) => {\n\t\t\tsetProcessing(true);\n\t\t\tsetError(\"\");\n\n\t\t\ttry {\n\t\t\t\t// Validate file size\n\t\t\t\tif (file.size > maxSize) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`File too large. Maximum size is ${Math.round(maxSize / 1024 / 1024)}MB`,\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\t// Validate file type\n\t\t\t\tif (\n\t\t\t\t\taccept &&\n\t\t\t\t\t!accept.split(\",\").some((type) => {\n\t\t\t\t\t\tconst cleanType = type.trim();\n\t\t\t\t\t\tif (cleanType.startsWith(\".\")) {\n\t\t\t\t\t\t\treturn file.name.toLowerCase().endsWith(cleanType.toLowerCase());\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn file.type === cleanType;\n\t\t\t\t\t})\n\t\t\t\t) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Invalid file type. Please select a file with extension: ${accept}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\t// Read file content\n\t\t\t\tconst content = await file.text();\n\n\t\t\t\tif (!content.trim()) {\n\t\t\t\t\tthrow new Error(\"File is empty\");\n\t\t\t\t}\n\n\t\t\t\t// Validate content\n\t\t\t\tconst validator = validateFile || defaultValidateFile;\n\t\t\t\tconst result = await validator(content, file);\n\n\t\t\t\tif (result.isValid) {\n\t\t\t\t\tonFileValidated?.(file, result);\n\t\t\t\t\tonFileSelect?.(file);\n\t\t\t\t} else {\n\t\t\t\t\tconst errorMsg = result.error || \"Invalid file format\";\n\t\t\t\t\tsetError(errorMsg);\n\t\t\t\t\tonError?.(errorMsg);\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\tconst errorMsg =\n\t\t\t\t\terr instanceof Error ? err.message : \"Failed to process file\";\n\t\t\t\tsetError(errorMsg);\n\t\t\t\tonError?.(errorMsg);\n\t\t\t} finally {\n\t\t\t\tsetProcessing(false);\n\t\t\t}\n\t\t},\n\t\t[\n\t\t\tmaxSize,\n\t\t\taccept,\n\t\t\tvalidateFile,\n\t\t\tdefaultValidateFile,\n\t\t\tonFileValidated,\n\t\t\tonError,\n\t\t\tonFileSelect,\n\t\t],\n\t);\n\n\tconst isDisabled = disabled || loading || processing;\n\n\treturn (\n\t\t<div className={cn(\"space-y-4\", className)}>\n\t\t\t{/* Drop Zone */}\n\t\t\t<DropZone\n\t\t\t\taccept={accept}\n\t\t\t\tdisabled={isDisabled}\n\t\t\t\tloading={processing}\n\t\t\t\theight={150}\n\t\t\t\ttitle=\"Import Backup File\"\n\t\t\t\tdescription={`Supports encrypted and unencrypted backups (${accept})`}\n\t\t\t\tdragActiveText=\"Drop your backup file here\"\n\t\t\t\tdragActiveDescription=\"Release to import\"\n\t\t\t\tloadingText=\"Processing file...\"\n\t\t\t\tmaxSize={maxSize}\n\t\t\t\tonFileSelect={(file) => processFile(file)}\n\t\t\t\tonError={(error) => {\n\t\t\t\t\tsetError(error);\n\t\t\t\t\tonError?.(error);\n\t\t\t\t}}\n\t\t\t/>\n\n\t\t\t{/* Error Display */}\n\t\t\t<ErrorDisplay error={error} />\n\n\t\t\t{/* File Format Info */}\n\t\t\t<Card className=\"bg-muted/50\">\n\t\t\t\t<CardContent className=\"pt-6\">\n\t\t\t\t\t<div className=\"space-y-2\">\n\t\t\t\t\t\t<h4 className=\"text-sm font-medium\">Supported Formats</h4>\n\t\t\t\t\t\t<div className=\"space-y-1\">\n\t\t\t\t\t\t\t<p className=\"text-xs text-muted-foreground\">\n\t\t\t\t\t\t\t\t• <strong>Encrypted JSON:</strong> Bitcoin Auth backup with\n\t\t\t\t\t\t\t\tencryption\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t<p className=\"text-xs text-muted-foreground\">\n\t\t\t\t\t\t\t\t• <strong>Unencrypted JSON:</strong> Raw BAP master backup\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t<p className=\"text-xs text-muted-foreground\">\n\t\t\t\t\t\t\t\t• <strong>Encrypted Text:</strong> Base64 encoded backup data\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t<p className=\"text-xs text-muted-foreground\">\n\t\t\t\t\t\t\t\t• <strong>Legacy formats:</strong> Older backup file structures\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</CardContent>\n\t\t\t</Card>\n\t\t</div>\n\t);\n}\n",
      "target": "<%- config.aliases.components %>/fileimport.tsx"
    }
  ]
}