{
  "name": "inscriptions-components-collectionform",
  "type": "registry:component",
  "dependencies": [
    "@radix-ui/react-icons"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/inscriptions/components/CollectionForm.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\n/**\n * CollectionForm Component\n *\n * Comprehensive form for creating NFT collections with all metadata\n * Uses localStorage for persistence and integrates all collection editors\n */\n\nimport { ImageIcon } from \"@radix-ui/react-icons\";\nimport type {\n\tCollectionSubTypeData,\n\tCollectionTraits,\n\tPreMAP,\n} from \"js-1sat-ord\";\nimport { useCallback, useEffect, useState } from \"react\";\nimport { cn } from \"../../../lib/utils.js\";\nimport { DropZone } from \"../../ui-components/DropZone.js\";\nimport { Button } from \"../../ui/button.js\";\nimport { Input } from \"../../ui/input.js\";\nimport { Separator } from \"../../ui/separator.js\";\nimport { Textarea } from \"../../ui/textarea.js\";\nimport type { CollectionData } from \"./CollectionMintButton.js\";\nimport { RarityEditor } from \"./RarityEditor.js\";\nimport { RoyaltyEditor } from \"./RoyaltyEditor.js\";\nimport { TraitEditor } from \"./TraitEditor.js\";\n\nexport interface CollectionFormProps {\n\t/** Storage key prefix for localStorage */\n\tstorageKeyPrefix?: string;\n\t/** Marketplace address for default royalty */\n\tmarketplaceAddress?: string;\n\t/** Callback when form is submitted */\n\tonSubmit?: (data: CollectionData, metadata: PreMAP) => void;\n\t/** Submit button text */\n\tsubmitText?: string;\n\t/** Whether to show the submit button */\n\tshowSubmit?: boolean;\n\t/** Maximum image size in bytes */\n\tmaxImageSize?: number;\n\t/** Maximum image dimension in pixels */\n\tmaxImageDimension?: number;\n}\n\nconst SUPPORTED_IMAGE_TYPES = [\n\t\"image/png\",\n\t\"image/jpeg\",\n\t\"image/jpg\",\n\t\"image/gif\",\n\t\"image/webp\",\n];\n\nexport function CollectionForm({\n\tstorageKeyPrefix = \"bigblocks-collection\",\n\tmarketplaceAddress = \"\",\n\tonSubmit,\n\tsubmitText = \"Create Collection\",\n\tshowSubmit = true,\n\tmaxImageSize = 1024 * 1024, // 1MB\n\tmaxImageDimension = 2048,\n}: CollectionFormProps) {\n\t// Basic info state with localStorage\n\tconst [name, setName] = useState(\"\");\n\tconst [description, setDescription] = useState(\"\");\n\tconst [quantity, setQuantity] = useState(\"\");\n\tconst [coverImage, setCoverImage] = useState<File | null>(null);\n\tconst [coverImagePreview, setCoverImagePreview] = useState<string | null>(\n\t\tnull,\n\t);\n\n\t// Form data from child components\n\tconst [rarities, setRarities] = useState<CollectionData[\"rarities\"]>([]);\n\tconst [traits, setTraits] = useState<CollectionTraits>({});\n\tconst [royalties, setRoyalties] = useState<CollectionData[\"royalties\"]>([]);\n\n\t// Validation state\n\tconst [imageError, setImageError] = useState<string>(\"\");\n\n\t// Load basic info from localStorage\n\tuseEffect(() => {\n\t\tif (typeof window !== \"undefined\") {\n\t\t\tconst storedName = localStorage.getItem(`${storageKeyPrefix}-name`);\n\t\t\tconst storedDescription = localStorage.getItem(\n\t\t\t\t`${storageKeyPrefix}-description`,\n\t\t\t);\n\t\t\tconst storedQuantity = localStorage.getItem(\n\t\t\t\t`${storageKeyPrefix}-quantity`,\n\t\t\t);\n\t\t\tconst storedImageData = localStorage.getItem(\n\t\t\t\t`${storageKeyPrefix}-image-data`,\n\t\t\t);\n\t\t\tconst storedImageName = localStorage.getItem(\n\t\t\t\t`${storageKeyPrefix}-image-name`,\n\t\t\t);\n\n\t\t\tif (storedName) setName(storedName);\n\t\t\tif (storedDescription) setDescription(storedDescription);\n\t\t\tif (storedQuantity) setQuantity(storedQuantity);\n\n\t\t\t// Restore image from data URL\n\t\t\tif (storedImageData && storedImageName) {\n\t\t\t\tsetCoverImagePreview(storedImageData);\n\t\t\t\t// Convert data URL back to File\n\t\t\t\tconst arr = storedImageData.split(\",\");\n\t\t\t\tconst mimeMatch = arr[0]?.match(/:(.*?);/);\n\t\t\t\tconst mime = mimeMatch ? mimeMatch[1] : \"image/png\";\n\t\t\t\tconst bstr = arr[1] ? atob(arr[1]) : \"\";\n\t\t\t\tlet n = bstr.length;\n\t\t\t\tconst u8arr = new Uint8Array(n);\n\t\t\t\twhile (n--) {\n\t\t\t\t\tu8arr[n] = bstr.charCodeAt(n);\n\t\t\t\t}\n\t\t\t\tconst file = new File([u8arr], storedImageName, { type: mime });\n\t\t\t\tsetCoverImage(file);\n\t\t\t}\n\t\t}\n\t}, [storageKeyPrefix]);\n\n\t// Save basic info to localStorage\n\tconst updateName = useCallback(\n\t\t(value: string) => {\n\t\t\tsetName(value);\n\t\t\tif (typeof window !== \"undefined\") {\n\t\t\t\tlocalStorage.setItem(`${storageKeyPrefix}-name`, value);\n\t\t\t}\n\t\t},\n\t\t[storageKeyPrefix],\n\t);\n\n\tconst updateDescription = useCallback(\n\t\t(value: string) => {\n\t\t\tsetDescription(value);\n\t\t\tif (typeof window !== \"undefined\") {\n\t\t\t\tlocalStorage.setItem(`${storageKeyPrefix}-description`, value);\n\t\t\t}\n\t\t},\n\t\t[storageKeyPrefix],\n\t);\n\n\tconst updateQuantity = useCallback(\n\t\t(value: string) => {\n\t\t\tsetQuantity(value);\n\t\t\tif (typeof window !== \"undefined\") {\n\t\t\t\tlocalStorage.setItem(`${storageKeyPrefix}-quantity`, value);\n\t\t\t}\n\t\t},\n\t\t[storageKeyPrefix],\n\t);\n\n\t// Handle image selection\n\tconst handleImageSelect = useCallback(\n\t\t(file: File) => {\n\t\t\tsetImageError(\"\");\n\n\t\t\t// Validate file type\n\t\t\tif (!SUPPORTED_IMAGE_TYPES.includes(file.type)) {\n\t\t\t\tsetImageError(\n\t\t\t\t\t\"Unsupported image type. Please use PNG, JPEG, GIF, or WebP.\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Validate file size\n\t\t\tif (file.size > maxImageSize) {\n\t\t\t\tsetImageError(\n\t\t\t\t\t`Image too large. Maximum size is ${maxImageSize / 1024 / 1024}MB.`,\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Create preview and validate dimensions\n\t\t\tconst reader = new FileReader();\n\t\t\treader.onload = (e) => {\n\t\t\t\tconst img = new Image();\n\t\t\t\timg.onload = () => {\n\t\t\t\t\t// Check if image is square\n\t\t\t\t\tif (img.width !== img.height) {\n\t\t\t\t\t\tsetImageError(\"Image must be square (same width and height).\");\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Check dimensions\n\t\t\t\t\tif (img.width > maxImageDimension || img.height > maxImageDimension) {\n\t\t\t\t\t\tsetImageError(\n\t\t\t\t\t\t\t`Image too large. Maximum dimensions are ${maxImageDimension}x${maxImageDimension}px.`,\n\t\t\t\t\t\t);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// All validations passed\n\t\t\t\t\tconst dataUrl = e.target?.result as string;\n\t\t\t\t\tsetCoverImage(file);\n\t\t\t\t\tsetCoverImagePreview(dataUrl);\n\n\t\t\t\t\t// Save to localStorage\n\t\t\t\t\tif (typeof window !== \"undefined\") {\n\t\t\t\t\t\tlocalStorage.setItem(`${storageKeyPrefix}-image-data`, dataUrl);\n\t\t\t\t\t\tlocalStorage.setItem(`${storageKeyPrefix}-image-name`, file.name);\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t\timg.src = e.target?.result as string;\n\t\t\t};\n\t\t\treader.readAsDataURL(file);\n\t\t},\n\t\t[maxImageSize, maxImageDimension, storageKeyPrefix],\n\t);\n\n\t// Remove image\n\tconst removeImage = useCallback(() => {\n\t\tsetCoverImage(null);\n\t\tsetCoverImagePreview(null);\n\t\tif (typeof window !== \"undefined\") {\n\t\t\tlocalStorage.removeItem(`${storageKeyPrefix}-image-data`);\n\t\t\tlocalStorage.removeItem(`${storageKeyPrefix}-image-name`);\n\t\t}\n\t}, [storageKeyPrefix]);\n\n\t// Handle form submission\n\tconst handleSubmit = useCallback(() => {\n\t\tif (!coverImage || !name || !description) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst collectionData: CollectionData = {\n\t\t\tname,\n\t\t\tdescription,\n\t\t\tquantity: quantity ? Number.parseInt(quantity) : undefined,\n\t\t\tcoverImage,\n\t\t\trarities,\n\t\t\ttraits,\n\t\t\troyalties,\n\t\t};\n\n\t\t// Build metadata\n\t\tconst metadata: PreMAP = {\n\t\t\tapp: \"1sat.market\",\n\t\t\ttype: \"ord\",\n\t\t\tsubType: \"collection\",\n\t\t\tname,\n\t\t};\n\n\t\t// Add royalties if present\n\t\tif (royalties.length > 0) {\n\t\t\tmetadata.royalties = royalties;\n\t\t}\n\n\t\t// Build subTypeData according to CollectionSubTypeData interface\n\t\tconst subTypeData: Partial<CollectionSubTypeData> = {\n\t\t\tdescription,\n\t\t\tquantity: quantity ? Number.parseInt(quantity) : 0, // 0 means unlimited\n\t\t\trarityLabels:\n\t\t\t\trarities.length > 0\n\t\t\t\t\t? rarities.map((r) => ({\n\t\t\t\t\t\t\tlabel: r.label,\n\t\t\t\t\t\t\tpercentage: r.percentage,\n\t\t\t\t\t\t\t...(r.items ? { items: r.items } : {}),\n\t\t\t\t\t\t}))\n\t\t\t\t\t: [],\n\t\t\ttraits: Object.keys(traits).length > 0 ? traits : {},\n\t\t};\n\n\t\tmetadata.subTypeData = subTypeData as CollectionSubTypeData;\n\n\t\tonSubmit?.(collectionData, metadata);\n\t}, [\n\t\tcoverImage,\n\t\tname,\n\t\tdescription,\n\t\tquantity,\n\t\trarities,\n\t\ttraits,\n\t\troyalties,\n\t\tonSubmit,\n\t]);\n\n\t// Check if form is valid\n\tconst isValid = !!(name && description && coverImage && !imageError);\n\n\treturn (\n\t\t<div className=\"flex flex-col gap-4\">\n\t\t\t{/* Basic Information */}\n\t\t\t<div className=\"flex flex-col gap-4\">\n\t\t\t\t<p className=\"text-lg font-bold\">Basic Information</p>\n\n\t\t\t\t<div className=\"flex flex-col gap-2\">\n\t\t\t\t\t<p className=\"text-sm font-medium\">Collection Name *</p>\n\t\t\t\t\t<Input\n\t\t\t\t\t\tplaceholder=\"e.g., Pixel Punks\"\n\t\t\t\t\t\tvalue={name}\n\t\t\t\t\t\tonChange={(e) => updateName(e.target.value)}\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\n\t\t\t\t<div className=\"flex flex-col gap-2\">\n\t\t\t\t\t<p className=\"text-sm font-medium\">Description *</p>\n\t\t\t\t\t<Textarea\n\t\t\t\t\t\tplaceholder=\"Describe your collection...\"\n\t\t\t\t\t\tvalue={description}\n\t\t\t\t\t\tonChange={(e) => updateDescription(e.target.value)}\n\t\t\t\t\t\tclassName=\"min-h-[100px]\"\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\n\t\t\t\t<div className=\"flex flex-col gap-2\">\n\t\t\t\t\t<p className=\"text-sm font-medium\">Total Supply (Optional)</p>\n\t\t\t\t\t<Input\n\t\t\t\t\t\tplaceholder=\"e.g., 10000\"\n\t\t\t\t\t\tvalue={quantity}\n\t\t\t\t\t\tonChange={(e) => updateQuantity(e.target.value)}\n\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\tmin=\"1\"\n\t\t\t\t\t/>\n\t\t\t\t\t<p className=\"text-xs text-muted-foreground\">\n\t\t\t\t\t\tLeave empty for unlimited supply\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\n\t\t\t\t<div className=\"flex flex-col gap-2\">\n\t\t\t\t\t<p className=\"text-sm font-medium\">Cover Image *</p>\n\t\t\t\t\t{coverImagePreview ? (\n\t\t\t\t\t\t<div className=\"flex flex-col gap-2\">\n\t\t\t\t\t\t\t<div className=\"flex flex-col items-center gap-2 p-3 border rounded-lg bg-muted\">\n\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\tsrc={coverImagePreview}\n\t\t\t\t\t\t\t\t\talt=\"Collection cover preview\"\n\t\t\t\t\t\t\t\t\tclassName=\"w-[200px] h-[200px] object-contain rounded-md\"\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t<p className=\"text-xs text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t{coverImage?.name}\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t<Button size=\"sm\" variant=\"secondary\" onClick={removeImage}>\n\t\t\t\t\t\t\t\t\tRemove\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<DropZone\n\t\t\t\t\t\t\tonFileSelect={handleImageSelect}\n\t\t\t\t\t\t\taccept={SUPPORTED_IMAGE_TYPES.join(\",\")}\n\t\t\t\t\t\t\tmaxSize={maxImageSize}\n\t\t\t\t\t\t\tmultiple={false}\n\t\t\t\t\t\t\ticon={<ImageIcon width=\"32\" height=\"32\" />}\n\t\t\t\t\t\t\ttitle=\"Drop image here or click to browse\"\n\t\t\t\t\t\t\tdescription={`Square image, max ${maxImageDimension}x${maxImageDimension}px, under ${maxImageSize / 1024 / 1024}MB`}\n\t\t\t\t\t\t/>\n\t\t\t\t\t)}\n\t\t\t\t\t{imageError && <p className=\"text-xs text-red-600\">{imageError}</p>}\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<Separator className=\"h-1\" />\n\n\t\t\t{/* Rarity Tiers */}\n\t\t\t<RarityEditor\n\t\t\t\tstorageKey={`${storageKeyPrefix}-rarities`}\n\t\t\t\ttotalQuantity={quantity ? Number.parseInt(quantity) : undefined}\n\t\t\t\tonChange={setRarities}\n\t\t\t/>\n\n\t\t\t<Separator className=\"h-1\" />\n\n\t\t\t{/* Trait Categories */}\n\t\t\t<TraitEditor\n\t\t\t\tstorageKey={`${storageKeyPrefix}-traits`}\n\t\t\t\ttotalQuantity={quantity ? Number.parseInt(quantity) : undefined}\n\t\t\t\tonChange={(newTraits) => setTraits(newTraits)}\n\t\t\t/>\n\n\t\t\t<Separator className=\"h-1\" />\n\n\t\t\t{/* Royalty Recipients */}\n\t\t\t<RoyaltyEditor\n\t\t\t\tstorageKey={`${storageKeyPrefix}-royalties`}\n\t\t\t\tmarketplaceAddress={marketplaceAddress}\n\t\t\t\tonChange={setRoyalties}\n\t\t\t/>\n\n\t\t\t{showSubmit && (\n\t\t\t\t<>\n\t\t\t\t\t<Separator className=\"h-1\" />\n\t\t\t\t\t<div className=\"flex justify-end\">\n\t\t\t\t\t\t<Button size=\"lg\" disabled={!isValid} onClick={handleSubmit}>\n\t\t\t\t\t\t\t{submitText}\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</div>\n\t\t\t\t</>\n\t\t\t)}\n\t\t</div>\n\t);\n}\n",
      "target": "<%- config.aliases.components %>/collectionform.tsx"
    }
  ]
}