{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "deploy-token",
  "title": "Deploy Token",
  "author": "Satchmo",
  "description": "BSV21 fungible token deployment block with symbol input, icon upload, max supply, decimals, fee estimate, and deploy action via @1sat/core deployBsv21Token",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "badge",
    "button",
    "card",
    "input",
    "label",
    "separator"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/deploy-token/index.tsx",
      "content": "\"use client\"\n\nimport { useCallback } from \"react\"\nimport { DeployTokenUI } from \"./ui\"\nimport {\n  useDeployToken,\n  type DeployTokenParams,\n  type DeployTokenResult,\n  type UseDeployTokenOptions,\n} from \"./use-deploy-token\"\n\n// ---------------------------------------------------------------------------\n// Re-exports\n// ---------------------------------------------------------------------------\n\nexport { DeployTokenUI, type DeployTokenUIProps, type DeployTokenFormState } from \"./ui\"\nexport {\n  useDeployToken,\n  type DeployTokenParams,\n  type DeployTokenResult,\n  type UseDeployTokenOptions,\n  type UseDeployTokenReturn,\n} from \"./use-deploy-token\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface DeployTokenProps {\n  /** Callback that executes the on-chain deploy action */\n  onDeploy: (params: DeployTokenParams) => Promise<DeployTokenResult>\n  /** Callback on successful deployment */\n  onSuccess?: (result: DeployTokenResult) => void\n  /** Callback on error */\n  onError?: (error: Error) => void\n  /** Default form values */\n  defaults?: UseDeployTokenOptions\n  /** Optional CSS class name */\n  className?: string\n  /** Callback to handle external links (e.g. open in system browser from a WebView) */\n  onExternalLink?: (url: string) => void\n}\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\n/**\n * Composed BSV21 token deploy block wiring the `useDeployToken` hook\n * to the `DeployTokenUI` presentation component.\n *\n * Pass an `onDeploy` handler that calls the underlying deploy action\n * (e.g. `deployBsv21Token` from `@1sat/core`) and return a result\n * containing `{ txid }` or `{ error }`.\n *\n * @example\n * ```tsx\n * import { DeployToken } from \"@/components/blocks/deploy-token\"\n * import { deployBsv21Token } from \"@1sat/core\"\n *\n * <DeployToken\n *   onDeploy={async (params) => {\n *     const tx = await deployBsv21Token({\n *       symbol: params.symbol,\n *       decimals: params.decimals,\n *       icon: { dataB64: params.iconBase64, contentType: params.iconContentType },\n *       initialDistribution: { address: myAddress, tokens: Number(params.maxSupply) },\n *       destinationAddress: myAddress,\n *       utxos: paymentUtxos,\n *     })\n *     return { txid: tx.id(\"hex\") }\n *   }}\n *   onSuccess={(r) => console.log(\"Deployed:\", r.txid)}\n * />\n * ```\n */\nexport function DeployToken({\n  onDeploy,\n  onSuccess,\n  onError,\n  defaults,\n  className,\n  onExternalLink,\n}: DeployTokenProps) {\n  const token = useDeployToken(defaults)\n\n  const handleDeploy = useCallback(() => {\n    void token.deploy(async (params) => {\n      const result = await onDeploy(params)\n\n      if (result.error) {\n        onError?.(new Error(result.error))\n      } else {\n        onSuccess?.(result)\n      }\n\n      return result\n    })\n  }, [token.deploy, onDeploy, onSuccess, onError])\n\n  return (\n    <DeployTokenUI\n      form={token.form}\n      onSymbolChange={token.setSymbol}\n      onMaxSupplyChange={token.setMaxSupply}\n      onDecimalsChange={token.setDecimals}\n      onIconSelect={token.selectIcon}\n      onIconRemove={token.removeIcon}\n      onDeploy={handleDeploy}\n      isDeploying={token.isDeploying}\n      feeEstimate={token.feeEstimate}\n      resultTxid={token.resultTxid}\n      errorMessage={token.errorMessage}\n      isValid={token.isValid}\n      className={className}\n      onExternalLink={onExternalLink}\n    />\n  )\n}\n",
      "type": "registry:block",
      "target": "~/components/blocks/deploy-token/index.tsx"
    },
    {
      "path": "registry/new-york/blocks/deploy-token/ui.tsx",
      "content": "\"use client\"\n\nimport { useCallback, type ChangeEvent } from \"react\"\nimport {\n  AlertCircle,\n  CheckCircle2,\n  ExternalLink,\n  Image as ImageIcon,\n  Loader2,\n  Rocket,\n  Upload,\n  X,\n} from \"lucide-react\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardFooter,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\"\nimport { Input } from \"@/components/ui/input\"\nimport { Label } from \"@/components/ui/label\"\nimport { Separator } from \"@/components/ui/separator\"\nimport { cn } from \"@/lib/utils\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface DeployTokenFormState {\n  /** Token symbol */\n  symbol: string\n  /** Maximum supply (whole tokens) */\n  maxSupply: string\n  /** Decimal precision (0-18) */\n  decimals: string\n  /** Icon file for inscription */\n  iconFile: File | null\n  /** Preview URL for the icon */\n  iconPreviewUrl: string | null\n}\n\nexport interface DeployTokenUIProps {\n  /** Current form state */\n  form: DeployTokenFormState\n  /** Callback to update symbol */\n  onSymbolChange: (value: string) => void\n  /** Callback to update max supply */\n  onMaxSupplyChange: (value: string) => void\n  /** Callback to update decimals */\n  onDecimalsChange: (value: string) => void\n  /** Callback to set icon file */\n  onIconSelect: (file: File) => void\n  /** Callback to remove the icon */\n  onIconRemove: () => void\n  /** Callback to trigger deploy */\n  onDeploy: () => void\n  /** Whether a deploy is in progress */\n  isDeploying: boolean\n  /** Fee estimate in satoshis */\n  feeEstimate: number\n  /** Deploy result txid */\n  resultTxid: string | null\n  /** Deploy error message */\n  errorMessage: string | null\n  /** Whether the form is valid for submission */\n  isValid: boolean\n  /** Optional CSS class name */\n  className?: string\n  /** Callback to handle external links (e.g. open in system browser from a WebView) */\n  onExternalLink?: (url: string) => void\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction formatNumber(value: string): string {\n  const num = Number(value)\n  if (Number.isNaN(num)) return value\n  return num.toLocaleString()\n}\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\n/**\n * Pure presentation component for BSV21 token deployment.\n *\n * Renders the form fields (symbol, icon upload, max supply, decimals),\n * fee estimate, and deploy button. All state management is handled by\n * the parent via props.\n */\nexport function DeployTokenUI({\n  form,\n  onSymbolChange,\n  onMaxSupplyChange,\n  onDecimalsChange,\n  onIconSelect,\n  onIconRemove,\n  onDeploy,\n  isDeploying,\n  feeEstimate,\n  resultTxid,\n  errorMessage,\n  isValid,\n  className,\n  onExternalLink,\n}: DeployTokenUIProps) {\n  const handleIconInput = useCallback(\n    (e: ChangeEvent<HTMLInputElement>) => {\n      const file = e.target.files?.[0]\n      if (!file) return\n      if (!file.type.startsWith(\"image/\")) return\n      onIconSelect(file)\n    },\n    [onIconSelect]\n  )\n\n  return (\n    <Card className={cn(\"w-full\", className)}>\n      <CardHeader>\n        <CardTitle>Deploy New Token</CardTitle>\n        <CardDescription>\n          Deploy a BSV21 fungible token. All tokens are minted to your wallet on\n          deployment.\n        </CardDescription>\n      </CardHeader>\n\n      <CardContent className=\"flex flex-col gap-6\">\n        {/* Symbol */}\n        <div className=\"flex flex-col gap-2\">\n          <div className=\"flex items-center justify-between\">\n            <Label htmlFor=\"deploy-token-symbol\">Symbol</Label>\n            <span className=\"text-xs text-muted-foreground\">\n              Not required to be unique\n            </span>\n          </div>\n          <Input\n            id=\"deploy-token-symbol\"\n            placeholder=\"e.g. MYTOKEN\"\n            value={form.symbol}\n            maxLength={32}\n            onKeyDown={(e) => {\n              if (e.key === \" \") e.preventDefault()\n            }}\n            onChange={(e) => onSymbolChange(e.target.value.replace(/\\s/g, \"\"))}\n            aria-label=\"Token symbol\"\n          />\n        </div>\n\n        {/* Icon upload */}\n        <div className=\"flex flex-col gap-2\">\n          <div className=\"flex items-center justify-between\">\n            <Label>Token Icon</Label>\n            <span className=\"text-xs text-muted-foreground\">\n              Square image recommended\n            </span>\n          </div>\n          <div className=\"flex items-center gap-4\">\n            {form.iconPreviewUrl ? (\n              <div className=\"relative size-10 flex-shrink-0 overflow-hidden rounded-lg border bg-muted\">\n                <img\n                  src={form.iconPreviewUrl}\n                  alt=\"Token icon preview\"\n                  className=\"h-full w-full object-cover\"\n                />\n                <Button\n                  type=\"button\"\n                  variant=\"destructive\"\n                  size=\"icon\"\n                  className=\"absolute -right-1 -top-1 size-5\"\n                  onClick={onIconRemove}\n                  aria-label=\"Remove icon\"\n                >\n                  <X data-icon=\"inline-start\" />\n                </Button>\n              </div>\n            ) : (\n              <div className=\"flex size-10 flex-shrink-0 items-center justify-center rounded-lg border border-dashed bg-muted/50\">\n                <ImageIcon className=\"size-5 text-muted-foreground/50\" />\n              </div>\n            )}\n            <div className=\"flex-1\">\n              <Label\n                htmlFor=\"deploy-token-icon\"\n                className=\"flex h-10 w-full cursor-pointer items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm transition-colors hover:bg-muted/50\"\n              >\n                <Upload className=\"size-4\" />\n                {form.iconFile ? \"Change Icon\" : \"Select Image\"}\n              </Label>\n              <input\n                type=\"file\"\n                id=\"deploy-token-icon\"\n                accept=\"image/*\"\n                className=\"sr-only\"\n                onChange={handleIconInput}\n              />\n            </div>\n          </div>\n        </div>\n\n        {/* Max Supply + Decimals row */}\n        <div className=\"grid grid-cols-2 gap-4\">\n          <div className=\"flex flex-col gap-2\">\n            <div className=\"flex items-center justify-between\">\n              <Label htmlFor=\"deploy-token-supply\">Max Supply</Label>\n              <span className=\"text-xs text-muted-foreground\">\n                Whole tokens\n              </span>\n            </div>\n            <Input\n              id=\"deploy-token-supply\"\n              type=\"text\"\n              inputMode=\"numeric\"\n              placeholder=\"21000000\"\n              value={form.maxSupply}\n              onChange={(e) =>\n                onMaxSupplyChange(e.target.value.replace(/[^0-9]/g, \"\"))\n              }\n              aria-label=\"Maximum token supply\"\n            />\n            {form.maxSupply && (\n              <p className=\"text-xs text-muted-foreground\">\n                {formatNumber(form.maxSupply)} tokens\n              </p>\n            )}\n          </div>\n\n          <div className=\"flex flex-col gap-2\">\n            <div className=\"flex items-center justify-between\">\n              <Label htmlFor=\"deploy-token-decimals\">Decimals</Label>\n              <span className=\"text-xs text-muted-foreground\">0-18</span>\n            </div>\n            <Input\n              id=\"deploy-token-decimals\"\n              type=\"number\"\n              min={0}\n              max={18}\n              placeholder=\"8\"\n              value={form.decimals}\n              onChange={(e) => onDecimalsChange(e.target.value)}\n              aria-label=\"Decimal precision\"\n            />\n          </div>\n        </div>\n\n        {/* Fee estimate */}\n        {isValid && (\n          <>\n            <Separator />\n            <div className=\"flex items-center justify-between rounded-md border bg-muted/50 px-4 py-3\">\n              <span className=\"text-sm text-muted-foreground\">\n                Estimated deploy fee\n              </span>\n              <Badge variant=\"secondary\">\n                ~{feeEstimate.toLocaleString()} sats\n              </Badge>\n            </div>\n          </>\n        )}\n\n        {/* Success */}\n        {resultTxid && (\n          <div className=\"flex items-start gap-3 rounded-md border border-primary/20 bg-primary/5 p-4\">\n            <CheckCircle2 className=\"mt-0.5 size-5 flex-shrink-0 text-primary\" />\n            <div className=\"min-w-0 flex flex-col gap-1\">\n              <p className=\"text-sm font-medium\">Token deployed successfully</p>\n              {onExternalLink ? (\n                <button\n                  type=\"button\"\n                  onClick={() => onExternalLink(`https://whatsonchain.com/tx/${resultTxid}`)}\n                  className=\"inline-flex items-center gap-1 transition-colors hover:text-foreground\"\n                >\n                  <Badge variant=\"outline\" className=\"max-w-full truncate text-xs font-mono\">\n                    {resultTxid}\n                  </Badge>\n                  <ExternalLink className=\"size-3 flex-shrink-0\" />\n                </button>\n              ) : (\n                <a\n                  href={`https://whatsonchain.com/tx/${resultTxid}`}\n                  target=\"_blank\"\n                  rel=\"noopener noreferrer\"\n                  className=\"inline-flex items-center gap-1 transition-colors hover:text-foreground\"\n                >\n                  <Badge variant=\"outline\" className=\"max-w-full truncate text-xs font-mono\">\n                    {resultTxid}\n                  </Badge>\n                  <ExternalLink className=\"size-3 flex-shrink-0\" />\n                </a>\n              )}\n            </div>\n          </div>\n        )}\n\n        {/* Error */}\n        {errorMessage && (\n          <div className=\"flex items-start gap-3 rounded-md border border-destructive/20 bg-destructive/5 p-4\">\n            <AlertCircle className=\"mt-0.5 size-5 flex-shrink-0 text-destructive\" />\n            <div className=\"flex flex-col gap-1\">\n              <p className=\"text-sm font-medium\">Deployment failed</p>\n              <p className=\"text-xs text-muted-foreground\">{errorMessage}</p>\n            </div>\n          </div>\n        )}\n      </CardContent>\n\n      <CardFooter>\n        <Button\n          className=\"w-full\"\n          onClick={onDeploy}\n          disabled={!isValid || isDeploying}\n          aria-busy={isDeploying}\n        >\n          {isDeploying ? (\n            <>\n              <Loader2 className=\"animate-spin\" data-icon=\"inline-start\" />\n              Deploying...\n            </>\n          ) : (\n            <>\n              <Rocket data-icon=\"inline-start\" />\n              Deploy Token\n            </>\n          )}\n        </Button>\n      </CardFooter>\n    </Card>\n  )\n}\n",
      "type": "registry:component",
      "target": "~/components/blocks/deploy-token/ui.tsx"
    },
    {
      "path": "registry/new-york/blocks/deploy-token/use-deploy-token.ts",
      "content": "import { useCallback, useMemo, useState } from \"react\"\nimport type { DeployTokenFormState } from \"./ui\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Parameters passed to the onDeploy callback */\nexport interface DeployTokenParams {\n  /** Token symbol */\n  symbol: string\n  /** Maximum supply as a string (raw integer, no separators) */\n  maxSupply: string\n  /** Decimal precision (0-18) */\n  decimals: number\n  /** Base64-encoded icon image content */\n  iconBase64: string\n  /** MIME type of the icon image */\n  iconContentType: string\n}\n\n/** Result returned from the deploy callback */\nexport interface DeployTokenResult {\n  /** Transaction ID of the deploy inscription */\n  txid?: string\n  /** Raw transaction hex */\n  rawtx?: string\n  /** Error message if deployment failed */\n  error?: string\n}\n\nexport interface UseDeployTokenOptions {\n  /** Default symbol value */\n  defaultSymbol?: string\n  /** Default max supply value */\n  defaultMaxSupply?: string\n  /** Default decimal precision */\n  defaultDecimals?: number\n}\n\nexport interface UseDeployTokenReturn {\n  /** Current form state for the UI */\n  form: DeployTokenFormState\n  /** Whether the form is valid for submission */\n  isValid: boolean\n  /** Whether a deploy is in progress */\n  isDeploying: boolean\n  /** Estimated fee in satoshis */\n  feeEstimate: number\n  /** Deploy result txid */\n  resultTxid: string | null\n  /** Error message */\n  errorMessage: string | null\n  /** Update the symbol */\n  setSymbol: (value: string) => void\n  /** Update max supply */\n  setMaxSupply: (value: string) => void\n  /** Update decimals */\n  setDecimals: (value: string) => void\n  /** Select an icon file */\n  selectIcon: (file: File) => void\n  /** Remove the icon */\n  removeIcon: () => void\n  /** Execute the deploy action */\n  deploy: (\n    handler: (params: DeployTokenParams) => Promise<DeployTokenResult>\n  ) => Promise<void>\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction fileToBase64(file: File): Promise<string> {\n  return new Promise((resolve, reject) => {\n    const reader = new FileReader()\n    reader.onload = () => {\n      const result = reader.result\n      if (typeof result !== \"string\") {\n        reject(new Error(\"Failed to read file\"))\n        return\n      }\n      const base64 = result.split(\",\")[1]\n      if (!base64) {\n        reject(new Error(\"Failed to encode file as base64\"))\n        return\n      }\n      resolve(base64)\n    }\n    reader.onerror = () => reject(new Error(\"Failed to read file\"))\n    reader.readAsDataURL(file)\n  })\n}\n\n/**\n * Estimate the deploy fee.\n *\n * A BSV21 deploy+mint inscription includes the JSON payload plus\n * the icon image data. The estimate accounts for the base transaction\n * overhead plus ~0.5 sat per byte of icon data.\n */\nfunction estimateDeployFee(iconSize: number): number {\n  const baseFee = 100 // base tx overhead for deploy inscription\n  const iconFee = Math.ceil(iconSize * 0.5)\n  return baseFee + iconFee\n}\n\nfunction parseDecimals(value: string): number {\n  if (value === \"\") return 8\n  const parsed = Number.parseInt(value, 10)\n  if (Number.isNaN(parsed)) return 8\n  return Math.max(0, Math.min(18, parsed))\n}\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\n/**\n * Manages form state, validation, icon handling, fee estimation, and deploy\n * execution for a BSV21 token deployment.\n *\n * The hook is pure logic with no UI. Pair it with `DeployTokenUI` for\n * the presentation layer.\n *\n * @example\n * ```ts\n * const token = useDeployToken({ defaultMaxSupply: \"21000000\" })\n *\n * // In the deploy handler:\n * token.deploy(async (params) => {\n *   const config: DeployBsv21TokenConfig = {\n *     symbol: params.symbol,\n *     decimals: params.decimals,\n *     icon: { dataB64: params.iconBase64, contentType: params.iconContentType },\n *     initialDistribution: { address: myAddress, tokens: Number(params.maxSupply) },\n *     destinationAddress: myAddress,\n *     utxos: paymentUtxos,\n *   }\n *   const tx = await deployBsv21Token(config)\n *   return { txid: tx.id(\"hex\") }\n * })\n * ```\n */\nexport function useDeployToken(\n  options: UseDeployTokenOptions = {}\n): UseDeployTokenReturn {\n  const {\n    defaultSymbol = \"\",\n    defaultMaxSupply = \"21000000\",\n    defaultDecimals = 8,\n  } = options\n\n  // Form fields\n  const [symbol, setSymbol] = useState(defaultSymbol)\n  const [maxSupply, setMaxSupply] = useState(defaultMaxSupply)\n  const [decimals, setDecimals] = useState(\n    defaultDecimals === 8 ? \"\" : String(defaultDecimals)\n  )\n  const [iconFile, setIconFile] = useState<File | null>(null)\n  const [iconPreviewUrl, setIconPreviewUrl] = useState<string | null>(null)\n\n  // Execution state\n  const [isDeploying, setIsDeploying] = useState(false)\n  const [resultTxid, setResultTxid] = useState<string | null>(null)\n  const [errorMessage, setErrorMessage] = useState<string | null>(null)\n\n  // Derived state\n  const isValid = useMemo(() => {\n    if (!symbol.trim()) return false\n    if (!iconFile) return false\n    if (!maxSupply || maxSupply === \"0\") return false\n    const dec = parseDecimals(decimals)\n    if (dec < 0 || dec > 18) return false\n    return true\n  }, [symbol, iconFile, maxSupply, decimals])\n\n  const feeEstimate = useMemo(() => {\n    if (!iconFile) return 0\n    return estimateDeployFee(iconFile.size)\n  }, [iconFile])\n\n  const form: DeployTokenFormState = useMemo(\n    () => ({\n      symbol,\n      maxSupply,\n      decimals,\n      iconFile,\n      iconPreviewUrl,\n    }),\n    [symbol, maxSupply, decimals, iconFile, iconPreviewUrl]\n  )\n\n  // Icon handlers\n  const selectIcon = useCallback(\n    (file: File) => {\n      if (iconPreviewUrl) {\n        URL.revokeObjectURL(iconPreviewUrl)\n      }\n      setIconFile(file)\n      setIconPreviewUrl(URL.createObjectURL(file))\n      setResultTxid(null)\n      setErrorMessage(null)\n    },\n    [iconPreviewUrl]\n  )\n\n  const removeIcon = useCallback(() => {\n    if (iconPreviewUrl) {\n      URL.revokeObjectURL(iconPreviewUrl)\n    }\n    setIconFile(null)\n    setIconPreviewUrl(null)\n  }, [iconPreviewUrl])\n\n  // Deploy execution\n  const deploy = useCallback(\n    async (\n      handler: (params: DeployTokenParams) => Promise<DeployTokenResult>\n    ) => {\n      if (!isValid || !iconFile) return\n\n      setIsDeploying(true)\n      setErrorMessage(null)\n      setResultTxid(null)\n\n      try {\n        const iconBase64 = await fileToBase64(iconFile)\n        const params: DeployTokenParams = {\n          symbol: symbol.trim(),\n          maxSupply,\n          decimals: parseDecimals(decimals),\n          iconBase64,\n          iconContentType: iconFile.type || \"image/png\",\n        }\n\n        const result = await handler(params)\n\n        if (result.error) {\n          setErrorMessage(result.error)\n        } else if (result.txid) {\n          setResultTxid(result.txid)\n        }\n      } catch (err) {\n        const message =\n          err instanceof Error ? err.message : \"Token deployment failed\"\n        setErrorMessage(message)\n      } finally {\n        setIsDeploying(false)\n      }\n    },\n    [isValid, iconFile, symbol, maxSupply, decimals]\n  )\n\n  return {\n    form,\n    isValid,\n    isDeploying,\n    feeEstimate,\n    resultTxid,\n    errorMessage,\n    setSymbol,\n    setMaxSupply,\n    setDecimals,\n    selectIcon,\n    removeIcon,\n    deploy,\n  }\n}\n",
      "type": "registry:component",
      "target": "~/components/blocks/deploy-token/use-deploy-token.ts"
    }
  ],
  "type": "registry:block"
}