{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "inscribe-file",
  "title": "Inscribe File",
  "author": "Satchmo",
  "description": "Full inscription flow with tabbed interface for file upload, BSV20 tokens, and BSV21 tokens. Includes content type override, BAP signing, metadata editor, and on-chain inscription via @1sat/actions",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "badge",
    "button",
    "card",
    "checkbox",
    "input",
    "label",
    "select",
    "separator",
    "tabs"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/inscribe-file/index.tsx",
      "content": "\"use client\"\n\nimport { useCallback, useMemo, useState } from \"react\"\nimport { AlertCircle, CheckCircle2, ExternalLink, Loader2 } 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 { Separator } from \"@/components/ui/separator\"\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\"\nimport { cn } from \"@/lib/utils\"\nimport { Bsv20Form, createDefaultBsv20Data, type Bsv20FormData } from \"./bsv20-form\"\nimport { Bsv21Form, createDefaultBsv21Data, type Bsv21FormData } from \"./bsv21-form\"\nimport { InscribeDropzone } from \"./inscribe-dropzone\"\nimport { InscribeForm, type MetadataEntry } from \"./inscribe-form\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type InscribeTab = \"file\" | \"bsv20\" | \"bsv21\"\n\nexport interface InscribeFileProps {\n  /** Callback with the inscribe action parameters when user clicks \"Inscribe\" */\n  onInscribe: (params: InscribeParams) => Promise<InscribeResult>\n  /** Callback on successful inscription */\n  onSuccess?: (result: InscribeResult) => void\n  /** Callback on error */\n  onError?: (error: Error) => void\n  /** Maximum file size in bytes (default: 10MB) */\n  maxFileSize?: number\n  /** Default tab to show */\n  defaultTab?: InscribeTab\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\nexport interface InscribeParams {\n  /** The inscription type */\n  type: InscribeTab\n  /** Base64-encoded file content (file tab) */\n  base64Content?: string\n  /** MIME type of the file */\n  contentType: string\n  /** MAP metadata key/value pairs */\n  map: Record<string, string>\n  /** Whether to sign with BAP identity (Sigma) */\n  signWithBAP?: boolean\n  /** BSV20-specific data */\n  bsv20?: Bsv20FormData\n  /** BSV21-specific data */\n  bsv21?: {\n    symbol: string\n    maxSupply: string\n    decimals: string\n    /** Base64-encoded icon content */\n    iconBase64?: string\n    /** Icon MIME type */\n    iconContentType?: string\n  }\n}\n\nexport interface InscribeResult {\n  /** Transaction ID of the inscription */\n  txid?: string\n  /** Raw transaction hex */\n  rawtx?: string\n  /** Error message if inscription failed */\n  error?: string\n}\n\n// ---------------------------------------------------------------------------\n// Re-exports for consumers\n// ---------------------------------------------------------------------------\n\nexport type { MetadataEntry } from \"./inscribe-form\"\nexport type { Bsv20FormData, Bsv20Mode } from \"./bsv20-form\"\nexport type { Bsv21FormData } from \"./bsv21-form\"\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      // Strip the data URL prefix to get raw base64\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/** Estimate inscription fee (1 sat per byte of content + base tx fee) */\nfunction estimateFee(fileSize: number): number {\n  const baseFee = 50\n  const perByteFee = Math.ceil(fileSize * 0.5)\n  return baseFee + perByteFee\n}\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\n/**\n * A complete inscription flow for uploading files, BSV20 tokens, and BSV21\n * tokens to the BSV blockchain.\n *\n * Includes a tabbed interface with:\n * - **File** tab: drag-and-drop file dropzone, content type override, BAP\n *   signing, MAP metadata editor, and fee estimate\n * - **BSV20** tab: mint existing tickers or deploy new ones\n * - **BSV21** tab: deploy new tokens with icon upload\n *\n * @example\n * ```tsx\n * <InscribeFile\n *   onInscribe={async (params) => {\n *     if (params.type === \"file\") {\n *       return await inscribe.execute(ctx, {\n *         base64Content: params.base64Content,\n *         contentType: params.contentType,\n *         map: params.map,\n *         signWithBAP: params.signWithBAP,\n *       })\n *     }\n *     // Handle bsv20/bsv21 params...\n *   }}\n *   onSuccess={(result) => console.log(\"txid:\", result.txid)}\n * />\n * ```\n */\nexport function InscribeFile({\n  onInscribe,\n  onSuccess,\n  onError,\n  maxFileSize = 10 * 1024 * 1024,\n  defaultTab = \"file\",\n  className,\n  onExternalLink,\n}: InscribeFileProps) {\n  const [activeTab, setActiveTab] = useState<InscribeTab>(defaultTab)\n\n  // File tab state\n  const [file, setFile] = useState<File | null>(null)\n  const [contentType, setContentType] = useState(\"application/octet-stream\")\n  const [signWithBAP, setSignWithBAP] = useState(false)\n  const [metadata, setMetadata] = useState<MetadataEntry[]>([])\n\n  // BSV20 tab state\n  const [bsv20Data, setBsv20Data] = useState<Bsv20FormData>(\n    createDefaultBsv20Data\n  )\n\n  // BSV21 tab state\n  const [bsv21Data, setBsv21Data] = useState<Bsv21FormData>(\n    createDefaultBsv21Data\n  )\n\n  // Shared state\n  const [isInscribing, setIsInscribing] = useState(false)\n  const [result, setResult] = useState<InscribeResult | null>(null)\n  const [error, setError] = useState<string | null>(null)\n\n  const feeEstimate = useMemo(() => {\n    if (activeTab === \"file\" && file) return estimateFee(file.size)\n    if (activeTab === \"bsv20\") return estimateFee(200)\n    if (activeTab === \"bsv21\") {\n      const iconSize = bsv21Data.icon?.size ?? 0\n      return estimateFee(200 + iconSize)\n    }\n    return 0\n  }, [activeTab, file, bsv21Data.icon])\n\n  const canInscribe = useMemo(() => {\n    if (activeTab === \"file\") return file !== null\n    if (activeTab === \"bsv20\") return bsv20Data.ticker.length > 0\n    if (activeTab === \"bsv21\") {\n      return (\n        bsv21Data.symbol.length > 0 &&\n        bsv21Data.icon !== null &&\n        bsv21Data.maxSupply.length > 0\n      )\n    }\n    return false\n  }, [activeTab, file, bsv20Data.ticker, bsv21Data])\n\n  const inscribeButtonLabel = useMemo(() => {\n    if (activeTab === \"bsv20\") {\n      return bsv20Data.mode === \"mint\" ? \"Mint Tokens\" : \"Deploy Ticker\"\n    }\n    if (activeTab === \"bsv21\") return \"Deploy Token\"\n    return \"Inscribe on Chain\"\n  }, [activeTab, bsv20Data.mode])\n\n  const handleFileSelect = useCallback((selectedFile: File) => {\n    setFile(selectedFile)\n    setContentType(selectedFile.type || \"application/octet-stream\")\n    setResult(null)\n    setError(null)\n  }, [])\n\n  const handleFileRemove = useCallback(() => {\n    setFile(null)\n    setContentType(\"application/octet-stream\")\n    setMetadata([])\n    setResult(null)\n    setError(null)\n  }, [])\n\n  const handleInscribe = useCallback(async () => {\n    setIsInscribing(true)\n    setError(null)\n    setResult(null)\n\n    try {\n      let params: InscribeParams\n\n      if (activeTab === \"file\") {\n        if (!file) return\n\n        const base64Content = await fileToBase64(file)\n        const map: Record<string, string> = {}\n        for (const entry of metadata) {\n          if (entry.key.trim() && entry.value.trim()) {\n            map[entry.key.trim()] = entry.value.trim()\n          }\n        }\n\n        params = {\n          type: \"file\",\n          base64Content,\n          contentType,\n          map,\n          signWithBAP: signWithBAP || undefined,\n        }\n      } else if (activeTab === \"bsv20\") {\n        params = {\n          type: \"bsv20\",\n          contentType: \"application/bsv-20\",\n          map: {},\n          bsv20: bsv20Data,\n        }\n      } else {\n        // bsv21\n        let iconBase64: string | undefined\n        let iconContentType: string | undefined\n        if (bsv21Data.icon) {\n          iconBase64 = await fileToBase64(bsv21Data.icon)\n          iconContentType = bsv21Data.icon.type || \"image/png\"\n        }\n\n        params = {\n          type: \"bsv21\",\n          contentType: \"application/bsv-20\",\n          map: {},\n          bsv21: {\n            symbol: bsv21Data.symbol,\n            maxSupply: bsv21Data.maxSupply,\n            decimals: bsv21Data.decimals,\n            iconBase64,\n            iconContentType,\n          },\n        }\n      }\n\n      const inscribeResult = await onInscribe(params)\n\n      if (inscribeResult.error) {\n        setError(inscribeResult.error)\n        const err = new Error(inscribeResult.error)\n        onError?.(err)\n      } else {\n        setResult(inscribeResult)\n        onSuccess?.(inscribeResult)\n      }\n    } catch (err) {\n      const errorMessage =\n        err instanceof Error ? err.message : \"Inscription failed\"\n      setError(errorMessage)\n      onError?.(err instanceof Error ? err : new Error(errorMessage))\n    } finally {\n      setIsInscribing(false)\n    }\n  }, [\n    activeTab,\n    file,\n    metadata,\n    contentType,\n    signWithBAP,\n    bsv20Data,\n    bsv21Data,\n    onInscribe,\n    onSuccess,\n    onError,\n  ])\n\n  return (\n    <Card className={cn(\"w-full\", className)}>\n      <CardHeader>\n        <CardTitle>Inscribe</CardTitle>\n        <CardDescription>\n          Create on-chain inscriptions on the BSV blockchain.\n        </CardDescription>\n      </CardHeader>\n\n      <CardContent className=\"flex flex-col gap-6\">\n        <Tabs\n          value={activeTab}\n          onValueChange={(value) => {\n            if (value === \"file\" || value === \"bsv20\" || value === \"bsv21\") {\n              setActiveTab(value)\n              setResult(null)\n              setError(null)\n            }\n          }}\n        >\n          <TabsList className=\"grid w-full grid-cols-3\">\n            <TabsTrigger value=\"file\">File</TabsTrigger>\n            <TabsTrigger value=\"bsv20\">BSV20</TabsTrigger>\n            <TabsTrigger value=\"bsv21\">BSV21</TabsTrigger>\n          </TabsList>\n\n          {/* ---- File Tab ---- */}\n          <TabsContent value=\"file\" className=\"mt-4 flex flex-col gap-6\">\n            <InscribeDropzone\n              file={file}\n              onFileSelect={handleFileSelect}\n              onFileRemove={handleFileRemove}\n              maxFileSize={maxFileSize}\n            />\n\n            {file && (\n              <InscribeForm\n                metadata={metadata}\n                onMetadataChange={setMetadata}\n                fileName={file.name}\n                contentType={contentType}\n                onContentTypeChange={setContentType}\n                signWithBAP={signWithBAP}\n                onSignWithBAPChange={setSignWithBAP}\n                onExternalLink={onExternalLink}\n              />\n            )}\n          </TabsContent>\n\n          {/* ---- BSV20 Tab ---- */}\n          <TabsContent value=\"bsv20\" className=\"mt-4\">\n            <Bsv20Form data={bsv20Data} onDataChange={setBsv20Data} onExternalLink={onExternalLink} />\n          </TabsContent>\n\n          {/* ---- BSV21 Tab ---- */}\n          <TabsContent value=\"bsv21\" className=\"mt-4\">\n            <Bsv21Form data={bsv21Data} onDataChange={setBsv21Data} onExternalLink={onExternalLink} />\n          </TabsContent>\n        </Tabs>\n\n        {/* Fee estimate */}\n        {feeEstimate > 0 && (\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 fee\n              </span>\n              <Badge variant=\"secondary\">\n                ~{feeEstimate.toLocaleString()} sats\n              </Badge>\n            </div>\n          </>\n        )}\n\n        {/* Success message */}\n        {result?.txid && (\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\">Inscription created</p>\n              {onExternalLink ? (\n                <button\n                  type=\"button\"\n                  onClick={() => onExternalLink(`https://whatsonchain.com/tx/${result.txid}`)}\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                    {result.txid}\n                  </Badge>\n                  <ExternalLink className=\"size-3 flex-shrink-0\" />\n                </button>\n              ) : (\n                <a\n                  href={`https://whatsonchain.com/tx/${result.txid}`}\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                    {result.txid}\n                  </Badge>\n                  <ExternalLink className=\"size-3 flex-shrink-0\" />\n                </a>\n              )}\n            </div>\n          </div>\n        )}\n\n        {/* Error message */}\n        {error && (\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\">Inscription failed</p>\n              <p className=\"text-xs text-muted-foreground\">{error}</p>\n            </div>\n          </div>\n        )}\n      </CardContent>\n\n      <CardFooter>\n        <Button\n          className=\"w-full\"\n          onClick={handleInscribe}\n          disabled={!canInscribe || isInscribing}\n          aria-busy={isInscribing}\n        >\n          {isInscribing ? (\n            <>\n              <Loader2 className=\"animate-spin\" data-icon=\"inline-start\" />\n              Inscribing...\n            </>\n          ) : (\n            inscribeButtonLabel\n          )}\n        </Button>\n      </CardFooter>\n    </Card>\n  )\n}\n",
      "type": "registry:block",
      "target": "~/components/blocks/inscribe-file/index.tsx"
    },
    {
      "path": "registry/new-york/blocks/inscribe-file/inscribe-dropzone.tsx",
      "content": "\"use client\"\n\nimport { useCallback, useEffect, useRef, useState, type DragEvent, type ChangeEvent } from \"react\"\nimport {\n  Code,\n  File as FileIcon,\n  Image as ImageIcon,\n  Music,\n  Trash2,\n  Upload,\n  Video,\n} from \"lucide-react\"\nimport { Button } from \"@/components/ui/button\"\nimport { Card, CardContent } from \"@/components/ui/card\"\nimport { Label } from \"@/components/ui/label\"\nimport { cn } from \"@/lib/utils\"\n\n/** Supported file categories for preview rendering */\ntype FileCategory = \"image\" | \"video\" | \"audio\" | \"code\" | \"other\"\n\nexport interface InscribeDropzoneProps {\n  /** Currently selected file */\n  file: File | null\n  /** Callback when a file is selected */\n  onFileSelect: (file: File) => void\n  /** Callback when the file is removed */\n  onFileRemove: () => void\n  /** Maximum file size in bytes (default: 10MB) */\n  maxFileSize?: number\n  /** Optional CSS class name */\n  className?: string\n}\n\nfunction categorizeFile(mimeType: string): FileCategory {\n  if (mimeType.startsWith(\"image/\")) return \"image\"\n  if (mimeType.startsWith(\"video/\")) return \"video\"\n  if (mimeType.startsWith(\"audio/\")) return \"audio\"\n  if (\n    mimeType.startsWith(\"text/\") ||\n    mimeType === \"application/json\" ||\n    mimeType === \"application/javascript\"\n  )\n    return \"code\"\n  return \"other\"\n}\n\nfunction FileCategoryIcon({\n  category,\n  className,\n}: {\n  category: FileCategory\n  className?: string\n}) {\n  switch (category) {\n    case \"image\":\n      return <ImageIcon className={className} />\n    case \"video\":\n      return <Video className={className} />\n    case \"audio\":\n      return <Music className={className} />\n    case \"code\":\n      return <Code className={className} />\n    case \"other\":\n      return <FileIcon className={className} />\n  }\n}\n\nfunction formatFileSize(bytes: number): string {\n  if (bytes < 1024) return `${bytes} B`\n  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`\n  return `${(bytes / (1024 * 1024)).toFixed(2)} MB`\n}\n\nexport function InscribeDropzone({\n  file,\n  onFileSelect,\n  onFileRemove,\n  maxFileSize = 10 * 1024 * 1024,\n  className,\n}: InscribeDropzoneProps) {\n  const [isDragging, setIsDragging] = useState(false)\n  const [previewUrl, setPreviewUrl] = useState<string | null>(null)\n  const [error, setError] = useState<string | null>(null)\n  const previewUrlRef = useRef<string | null>(null)\n\n  // Revoke object URL on unmount to prevent memory leaks\n  useEffect(() => {\n    return () => {\n      if (previewUrlRef.current) {\n        URL.revokeObjectURL(previewUrlRef.current)\n      }\n    }\n  }, [])\n\n  const handleFile = useCallback(\n    (selectedFile: File) => {\n      setError(null)\n\n      if (selectedFile.size > maxFileSize) {\n        setError(\n          `File too large. Maximum size is ${formatFileSize(maxFileSize)}.`\n        )\n        return\n      }\n\n      // Revoke previous preview URL before creating new one\n      if (previewUrlRef.current) {\n        URL.revokeObjectURL(previewUrlRef.current)\n        previewUrlRef.current = null\n      }\n\n      if (selectedFile.type.startsWith(\"image/\")) {\n        const url = URL.createObjectURL(selectedFile)\n        previewUrlRef.current = url\n        setPreviewUrl(url)\n      } else {\n        setPreviewUrl(null)\n      }\n\n      onFileSelect(selectedFile)\n    },\n    [maxFileSize, onFileSelect]\n  )\n\n  const handleRemove = useCallback(() => {\n    if (previewUrlRef.current) {\n      URL.revokeObjectURL(previewUrlRef.current)\n      previewUrlRef.current = null\n    }\n    setPreviewUrl(null)\n    setError(null)\n    onFileRemove()\n  }, [onFileRemove])\n\n  const handleDrop = useCallback(\n    (e: DragEvent<HTMLDivElement>) => {\n      e.preventDefault()\n      setIsDragging(false)\n\n      const droppedFile = e.dataTransfer.files[0]\n      if (droppedFile) {\n        handleFile(droppedFile)\n      }\n    },\n    [handleFile]\n  )\n\n  const handleDragOver = useCallback((e: DragEvent<HTMLDivElement>) => {\n    e.preventDefault()\n    setIsDragging(true)\n  }, [])\n\n  const handleDragLeave = useCallback((e: DragEvent<HTMLDivElement>) => {\n    e.preventDefault()\n    setIsDragging(false)\n  }, [])\n\n  const handleInputChange = useCallback(\n    (e: ChangeEvent<HTMLInputElement>) => {\n      const selectedFile = e.target.files?.[0]\n      if (selectedFile) {\n        handleFile(selectedFile)\n      }\n    },\n    [handleFile]\n  )\n\n  if (file) {\n    const category = categorizeFile(file.type)\n\n    return (\n      <div className={cn(\"flex flex-col gap-3\", className)}>\n        <Card>\n          <CardContent className=\"flex items-start gap-4 p-4\">\n            {/* Preview */}\n            <div className=\"relative size-20 flex-shrink-0 overflow-hidden rounded-md border bg-muted\">\n              {previewUrl ? (\n                <img\n                  src={previewUrl}\n                  alt=\"File preview\"\n                  className=\"h-full w-full object-cover\"\n                />\n              ) : (\n                <div className=\"flex h-full w-full items-center justify-center\">\n                  <FileCategoryIcon\n                    category={category}\n                    className=\"size-8 text-muted-foreground\"\n                  />\n                </div>\n              )}\n            </div>\n\n            {/* File info */}\n            <div className=\"flex-1 min-w-0 flex flex-col gap-1\">\n              <p className=\"text-sm font-medium truncate\">{file.name}</p>\n              <p className=\"text-xs text-muted-foreground\">\n                {file.type || \"Unknown type\"}\n              </p>\n              <p className=\"text-xs text-muted-foreground\">\n                {formatFileSize(file.size)}\n              </p>\n            </div>\n\n            {/* Remove button */}\n            <Button\n              variant=\"ghost\"\n              size=\"icon\"\n              onClick={handleRemove}\n              className=\"size-8 text-muted-foreground hover:text-destructive\"\n              aria-label=\"Remove file\"\n            >\n              <Trash2 data-icon=\"inline-start\" />\n            </Button>\n          </CardContent>\n        </Card>\n      </div>\n    )\n  }\n\n  return (\n    <div className={cn(\"flex flex-col gap-2\", className)}>\n      <div\n        onDrop={handleDrop}\n        onDragOver={handleDragOver}\n        onDragLeave={handleDragLeave}\n        className={cn(\n          \"relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed p-8 transition-colors\",\n          isDragging\n            ? \"border-primary bg-primary/5\"\n            : \"border-muted-foreground/25 hover:border-muted-foreground/50\"\n        )}\n      >\n        <div className=\"flex flex-col items-center gap-3 text-center\">\n          <div className=\"rounded-full bg-muted p-3\">\n            <Upload className=\"size-6 text-muted-foreground\" />\n          </div>\n          <div className=\"flex flex-col gap-1\">\n            <p className=\"text-sm font-medium\">\n              Drop a file here or click to browse\n            </p>\n            <p className=\"text-xs text-muted-foreground\">\n              Any file type up to {formatFileSize(maxFileSize)}\n            </p>\n          </div>\n          <Label htmlFor=\"inscribe-file-input\" className=\"sr-only\">\n            Choose file\n          </Label>\n          <input\n            id=\"inscribe-file-input\"\n            type=\"file\"\n            className=\"absolute inset-0 cursor-pointer opacity-0\"\n            onChange={handleInputChange}\n            aria-label=\"Choose file to inscribe\"\n          />\n        </div>\n      </div>\n\n      {error && (\n        <p className=\"text-sm text-destructive\" role=\"alert\">\n          {error}\n        </p>\n      )}\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "~/components/blocks/inscribe-file/inscribe-dropzone.tsx"
    },
    {
      "path": "registry/new-york/blocks/inscribe-file/inscribe-form.tsx",
      "content": "\"use client\"\n\nimport { useCallback } from \"react\"\nimport { Plus, X } from \"lucide-react\"\nimport { Button } from \"@/components/ui/button\"\nimport { Card, CardContent } from \"@/components/ui/card\"\nimport { Checkbox } from \"@/components/ui/checkbox\"\nimport { Input } from \"@/components/ui/input\"\nimport { Label } from \"@/components/ui/label\"\nimport { cn } from \"@/lib/utils\"\nimport { ContentTypeSelect } from \"./content-type-select\"\n\nexport interface MetadataEntry {\n  /** Unique identifier for the entry */\n  id: string\n  /** MAP key (alphanumeric only) */\n  key: string\n  /** MAP value */\n  value: string\n}\n\nexport interface InscribeFormProps {\n  /** Current metadata entries */\n  metadata: MetadataEntry[]\n  /** Callback when metadata changes */\n  onMetadataChange: (metadata: MetadataEntry[]) => void\n  /** The selected file name, used for auto-populating \"name\" key */\n  fileName?: string\n  /** Current content type (MIME) */\n  contentType: string\n  /** Callback when content type changes */\n  onContentTypeChange: (contentType: string) => void\n  /** Whether BAP sigma signing is enabled */\n  signWithBAP: boolean\n  /** Callback when BAP signing toggle changes */\n  onSignWithBAPChange: (enabled: boolean) => void\n  /** Callback to handle external links (e.g. open in system browser from a WebView) */\n  onExternalLink?: (url: string) => void\n  /** Optional CSS class name */\n  className?: string\n}\n\nfunction generateId(): string {\n  return `meta-${Math.random().toString(36).slice(2, 9)}-${Date.now()}`\n}\n\nexport function InscribeForm({\n  metadata,\n  onMetadataChange,\n  fileName,\n  contentType,\n  onContentTypeChange,\n  signWithBAP,\n  onSignWithBAPChange,\n  onExternalLink,\n  className,\n}: InscribeFormProps) {\n  const handleAdd = useCallback(() => {\n    const newEntries = [...metadata]\n\n    // Auto-suggest \"name\" key with file name if not already present\n    if (\n      newEntries.length === 0 &&\n      fileName &&\n      !newEntries.some((m) => m.key === \"name\")\n    ) {\n      newEntries.push({\n        id: generateId(),\n        key: \"name\",\n        value: fileName,\n      })\n    }\n\n    // Always add an empty row for the user\n    newEntries.push({\n      id: generateId(),\n      key: \"\",\n      value: \"\",\n    })\n\n    onMetadataChange(newEntries)\n  }, [metadata, onMetadataChange, fileName])\n\n  const handleUpdate = useCallback(\n    (id: string, field: \"key\" | \"value\", newValue: string) => {\n      onMetadataChange(\n        metadata.map((entry) => {\n          if (entry.id !== id) return entry\n          return {\n            ...entry,\n            [field]:\n              field === \"key\"\n                ? newValue.replace(/[^a-zA-Z0-9]/g, \"\")\n                : newValue,\n          }\n        })\n      )\n    },\n    [metadata, onMetadataChange]\n  )\n\n  const handleRemove = useCallback(\n    (id: string) => {\n      onMetadataChange(metadata.filter((entry) => entry.id !== id))\n    },\n    [metadata, onMetadataChange]\n  )\n\n  return (\n    <div className={cn(\"flex flex-col gap-4\", className)}>\n      {/* Content type override */}\n      <ContentTypeSelect\n        value={contentType}\n        onValueChange={onContentTypeChange}\n      />\n\n      {/* BAP signing checkbox */}\n      <Card>\n        <CardContent className=\"flex items-center gap-3 px-4 py-3\">\n          <Checkbox\n            id=\"bap-signing\"\n            checked={signWithBAP}\n            onCheckedChange={(checked) => {\n              if (typeof checked === \"boolean\") {\n                onSignWithBAPChange(checked)\n              }\n            }}\n          />\n          <div className=\"grid gap-0.5 leading-none\">\n            <Label htmlFor=\"bap-signing\" className=\"cursor-pointer text-sm\">\n              Sign with BAP identity (Sigma)\n            </Label>\n            <p className=\"text-xs text-muted-foreground\">\n              Attach a cryptographic identity proof to this inscription.\n            </p>\n          </div>\n        </CardContent>\n      </Card>\n\n      {/* Metadata editor */}\n      <div className=\"flex flex-col gap-3\">\n        <div className=\"flex items-center justify-between\">\n          <Label>Metadata (MAP Protocol)</Label>\n          <Button\n            type=\"button\"\n            size=\"sm\"\n            variant=\"outline\"\n            onClick={handleAdd}\n            className=\"h-8 gap-1\"\n          >\n            <Plus data-icon=\"inline-start\" />\n            Add Field\n          </Button>\n        </div>\n\n        <div className=\"flex flex-col gap-2\">\n          {metadata.map((entry) => (\n            <div key={entry.id} className=\"flex items-center gap-2\">\n              <Input\n                placeholder=\"Key\"\n                value={entry.key}\n                onChange={(e) => handleUpdate(entry.id, \"key\", e.target.value)}\n                className=\"flex-1\"\n                aria-label=\"Metadata key\"\n              />\n              <Input\n                placeholder=\"Value\"\n                value={entry.value}\n                onChange={(e) =>\n                  handleUpdate(entry.id, \"value\", e.target.value)\n                }\n                className=\"flex-1\"\n                aria-label=\"Metadata value\"\n              />\n              <Button\n                type=\"button\"\n                size=\"icon\"\n                variant=\"ghost\"\n                onClick={() => handleRemove(entry.id)}\n                className=\"size-10 flex-shrink-0 text-muted-foreground hover:text-destructive\"\n                aria-label=\"Remove metadata field\"\n              >\n                <X data-icon=\"inline-start\" />\n              </Button>\n            </div>\n          ))}\n\n          {metadata.length === 0 && (\n            <div className=\"rounded-md border border-dashed py-4 text-center text-sm text-muted-foreground\">\n              No metadata added. Click &quot;Add Field&quot; to attach key/value\n              pairs.\n            </div>\n          )}\n        </div>\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "~/components/blocks/inscribe-file/inscribe-form.tsx"
    },
    {
      "path": "registry/new-york/blocks/inscribe-file/content-type-select.tsx",
      "content": "\"use client\"\n\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\"\nimport { Label } from \"@/components/ui/label\"\nimport { cn } from \"@/lib/utils\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ContentTypeSelectProps {\n  /** Currently selected content type */\n  value: string\n  /** Callback when content type changes */\n  onValueChange: (value: string) => void\n  /** Optional CSS class name */\n  className?: string\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst CONTENT_TYPE_OPTIONS = [\n  { value: \"image/png\", label: \"PNG Image\" },\n  { value: \"image/jpeg\", label: \"JPEG Image\" },\n  { value: \"image/gif\", label: \"GIF Image\" },\n  { value: \"image/svg+xml\", label: \"SVG Image\" },\n  { value: \"image/webp\", label: \"WebP Image\" },\n  { value: \"text/plain\", label: \"Plain Text\" },\n  { value: \"text/html\", label: \"HTML\" },\n  { value: \"text/markdown\", label: \"Markdown\" },\n  { value: \"application/json\", label: \"JSON\" },\n  { value: \"video/mp4\", label: \"MP4 Video\" },\n  { value: \"audio/mpeg\", label: \"MP3 Audio\" },\n  { value: \"application/octet-stream\", label: \"Binary / Other\" },\n] as const\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\n/**\n * Dropdown for selecting or overriding the MIME content type of a file\n * to be inscribed. Auto-detects from the uploaded file but allows manual\n * override for edge cases (e.g. SVG uploaded as application/xml).\n */\nexport function ContentTypeSelect({\n  value,\n  onValueChange,\n  className,\n}: ContentTypeSelectProps) {\n  return (\n    <div className={cn(\"grid gap-2\", className)}>\n      <Label htmlFor=\"content-type-select\">Content Type</Label>\n      <Select value={value} onValueChange={onValueChange}>\n        <SelectTrigger id=\"content-type-select\">\n          <SelectValue placeholder=\"Select content type\" />\n        </SelectTrigger>\n        <SelectContent>\n          {CONTENT_TYPE_OPTIONS.map((option) => (\n            <SelectItem key={option.value} value={option.value}>\n              <span className=\"flex items-center gap-2\">\n                <span className=\"text-xs text-muted-foreground font-mono\">\n                  {option.value}\n                </span>\n                <span className=\"sr-only\">{option.label}</span>\n              </span>\n            </SelectItem>\n          ))}\n        </SelectContent>\n      </Select>\n      <p className=\"text-xs text-muted-foreground\">\n        Auto-detected from file. Override if needed.\n      </p>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "~/components/blocks/inscribe-file/content-type-select.tsx"
    },
    {
      "path": "registry/new-york/blocks/inscribe-file/bsv20-form.tsx",
      "content": "\"use client\"\n\nimport { useCallback, useState } from \"react\"\nimport { Button } from \"@/components/ui/button\"\nimport { Input } from \"@/components/ui/input\"\nimport { Label } from \"@/components/ui/label\"\nimport { cn } from \"@/lib/utils\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type Bsv20Mode = \"mint\" | \"deploy\"\n\nexport interface Bsv20FormData {\n  /** Whether minting an existing ticker or deploying a new one */\n  mode: Bsv20Mode\n  /** Ticker symbol (max 4 chars, uppercase) */\n  ticker: string\n  /** Amount to mint (mint mode only) */\n  amount: string\n  /** Max supply (deploy mode only) */\n  maxSupply: string\n  /** Mint limit per transaction (deploy mode only) */\n  mintLimit: string\n  /** Decimal precision (deploy mode only) */\n  decimals: string\n}\n\nexport interface Bsv20FormProps {\n  /** Current form data */\n  data: Bsv20FormData\n  /** Callback when any field changes */\n  onDataChange: (data: Bsv20FormData) => void\n  /** Callback to handle external links (e.g. open in system browser from a WebView) */\n  onExternalLink?: (url: string) => void\n  /** Optional CSS class name */\n  className?: string\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction createDefaultBsv20Data(): Bsv20FormData {\n  return {\n    mode: \"mint\",\n    ticker: \"\",\n    amount: \"\",\n    maxSupply: \"21000000\",\n    mintLimit: \"1000\",\n    decimals: \"0\",\n  }\n}\n\nexport { createDefaultBsv20Data }\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\n/**\n * Form for BSV20 fungible token inscription — supports both minting existing\n * tickers and deploying new ones.\n */\nexport function Bsv20Form({ data, onDataChange, onExternalLink, className }: Bsv20FormProps) {\n  const updateField = useCallback(\n    <K extends keyof Bsv20FormData>(field: K, value: Bsv20FormData[K]) => {\n      onDataChange({ ...data, [field]: value })\n    },\n    [data, onDataChange]\n  )\n\n  return (\n    <div className={cn(\"flex flex-col gap-4\", className)}>\n      {/* Mode toggle */}\n      <div className=\"flex items-center justify-between\">\n        <Label>Mode</Label>\n        <div className=\"flex rounded-lg border bg-muted p-1\">\n          <Button\n            type=\"button\"\n            variant={data.mode === \"mint\" ? \"secondary\" : \"ghost\"}\n            size=\"sm\"\n            onClick={() => updateField(\"mode\", \"mint\")}\n            className=\"h-7 text-xs\"\n          >\n            Mint\n          </Button>\n          <Button\n            type=\"button\"\n            variant={data.mode === \"deploy\" ? \"secondary\" : \"ghost\"}\n            size=\"sm\"\n            onClick={() => updateField(\"mode\", \"deploy\")}\n            className=\"h-7 text-xs\"\n          >\n            Deploy\n          </Button>\n        </div>\n      </div>\n\n      <p className=\"text-sm text-muted-foreground\">\n        {data.mode === \"mint\"\n          ? \"Mint tokens from an existing BSV20 ticker.\"\n          : \"Deploy a new BSV20 ticker to the blockchain.\"}\n      </p>\n\n      {/* Ticker */}\n      <div className=\"grid gap-2\">\n        <Label htmlFor=\"bsv20-ticker\">Ticker</Label>\n        <Input\n          id=\"bsv20-ticker\"\n          placeholder=\"e.g. PEPE\"\n          value={data.ticker}\n          onChange={(e) =>\n            updateField(\n              \"ticker\",\n              e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, \"\").slice(0, 4)\n            )\n          }\n          maxLength={4}\n          aria-describedby=\"bsv20-ticker-hint\"\n        />\n        <p id=\"bsv20-ticker-hint\" className=\"text-xs text-muted-foreground\">\n          Up to 4 uppercase characters.\n        </p>\n      </div>\n\n      {data.mode === \"mint\" ? (\n        /* Mint fields */\n        <div className=\"grid gap-2\">\n          <Label htmlFor=\"bsv20-amount\">Amount</Label>\n          <Input\n            id=\"bsv20-amount\"\n            type=\"text\"\n            inputMode=\"numeric\"\n            pattern=\"[0-9]*\"\n            placeholder=\"1000\"\n            value={data.amount}\n            onChange={(e) =>\n              updateField(\"amount\", e.target.value.replace(/[^0-9]/g, \"\"))\n            }\n          />\n        </div>\n      ) : (\n        /* Deploy fields */\n        <>\n          <div className=\"grid gap-2\">\n            <Label htmlFor=\"bsv20-max\">Max Supply</Label>\n            <Input\n              id=\"bsv20-max\"\n              type=\"text\"\n              inputMode=\"numeric\"\n              pattern=\"[0-9]*\"\n              placeholder=\"21000000\"\n              value={data.maxSupply}\n              onChange={(e) =>\n                updateField(\"maxSupply\", e.target.value.replace(/[^0-9]/g, \"\"))\n              }\n            />\n          </div>\n\n          <div className=\"grid gap-2\">\n            <Label htmlFor=\"bsv20-limit\">Mint Limit</Label>\n            <Input\n              id=\"bsv20-limit\"\n              type=\"text\"\n              inputMode=\"numeric\"\n              pattern=\"[0-9]*\"\n              placeholder=\"1000\"\n              value={data.mintLimit}\n              onChange={(e) =>\n                updateField(\"mintLimit\", e.target.value.replace(/[^0-9]/g, \"\"))\n              }\n            />\n            <p className=\"text-xs text-muted-foreground\">\n              Max tokens per mint transaction.\n            </p>\n          </div>\n\n          <div className=\"grid gap-2\">\n            <Label htmlFor=\"bsv20-decimals\">Decimals</Label>\n            <Input\n              id=\"bsv20-decimals\"\n              type=\"text\"\n              inputMode=\"numeric\"\n              pattern=\"[0-9]*\"\n              placeholder=\"0\"\n              value={data.decimals}\n              onChange={(e) =>\n                updateField(\"decimals\", e.target.value.replace(/[^0-9]/g, \"\"))\n              }\n            />\n          </div>\n        </>\n      )}\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "~/components/blocks/inscribe-file/bsv20-form.tsx"
    },
    {
      "path": "registry/new-york/blocks/inscribe-file/bsv21-form.tsx",
      "content": "\"use client\"\n\nimport { useCallback, useEffect, useRef, type ChangeEvent } from \"react\"\nimport { Image as ImageIcon, Upload, X } from \"lucide-react\"\nimport { Button } from \"@/components/ui/button\"\nimport { Card, CardContent } from \"@/components/ui/card\"\nimport { Input } from \"@/components/ui/input\"\nimport { Label } from \"@/components/ui/label\"\nimport { cn } from \"@/lib/utils\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface Bsv21FormData {\n  /** Token symbol (no spaces) */\n  symbol: string\n  /** Token icon file */\n  icon: File | null\n  /** Preview URL for the icon (created via URL.createObjectURL) */\n  iconPreviewUrl: string | null\n  /** Maximum token supply */\n  maxSupply: string\n  /** Decimal precision (default 8) */\n  decimals: string\n}\n\nexport interface Bsv21FormProps {\n  /** Current form data */\n  data: Bsv21FormData\n  /** Callback when any field changes */\n  onDataChange: (data: Bsv21FormData) => void\n  /** Callback to handle external links (e.g. open in system browser from a WebView) */\n  onExternalLink?: (url: string) => void\n  /** Optional CSS class name */\n  className?: string\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction createDefaultBsv21Data(): Bsv21FormData {\n  return {\n    symbol: \"\",\n    icon: null,\n    iconPreviewUrl: null,\n    maxSupply: \"21000000\",\n    decimals: \"8\",\n  }\n}\n\nexport { createDefaultBsv21Data }\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\n/**\n * Form for deploying a new BSV21 token. Includes symbol input, icon upload\n * with live preview, max supply, and decimal precision.\n */\nexport function Bsv21Form({ data, onDataChange, onExternalLink, className }: Bsv21FormProps) {\n  const lastPreviewUrlRef = useRef<string | null>(null)\n\n  // Track the current preview URL for unmount cleanup\n  useEffect(() => {\n    lastPreviewUrlRef.current = data.iconPreviewUrl\n  }, [data.iconPreviewUrl])\n\n  // Revoke object URL on unmount to prevent memory leaks\n  useEffect(() => {\n    return () => {\n      if (lastPreviewUrlRef.current) {\n        URL.revokeObjectURL(lastPreviewUrlRef.current)\n      }\n    }\n  }, [])\n\n  const updateField = useCallback(\n    <K extends keyof Bsv21FormData>(field: K, value: Bsv21FormData[K]) => {\n      onDataChange({ ...data, [field]: value })\n    },\n    [data, onDataChange]\n  )\n\n  const handleIconSelect = useCallback(\n    (e: ChangeEvent<HTMLInputElement>) => {\n      const selectedFile = e.target.files?.[0]\n      if (!selectedFile) return\n\n      if (!selectedFile.type.startsWith(\"image/\")) {\n        return\n      }\n\n      // Revoke previous preview URL\n      if (data.iconPreviewUrl) {\n        URL.revokeObjectURL(data.iconPreviewUrl)\n      }\n\n      const previewUrl = URL.createObjectURL(selectedFile)\n      onDataChange({\n        ...data,\n        icon: selectedFile,\n        iconPreviewUrl: previewUrl,\n      })\n    },\n    [data, onDataChange]\n  )\n\n  const handleIconRemove = useCallback(() => {\n    if (data.iconPreviewUrl) {\n      URL.revokeObjectURL(data.iconPreviewUrl)\n    }\n    onDataChange({\n      ...data,\n      icon: null,\n      iconPreviewUrl: null,\n    })\n  }, [data, onDataChange])\n\n  return (\n    <div className={cn(\"flex flex-col gap-4\", className)}>\n      <p className=\"text-sm text-muted-foreground\">\n        Deploy a new BSV21 token with an icon. All tokens are minted to your\n        wallet on deployment.\n      </p>\n\n      {/* Symbol */}\n      <div className=\"grid gap-2\">\n        <div className=\"flex items-center justify-between\">\n          <Label htmlFor=\"bsv21-symbol\">Symbol</Label>\n          <span className=\"text-xs text-muted-foreground\">\n            Does not need to be unique\n          </span>\n        </div>\n        <Input\n          id=\"bsv21-symbol\"\n          placeholder=\"e.g. MYTOKEN\"\n          value={data.symbol}\n          maxLength={255}\n          onKeyDown={(e) => {\n            if (e.key === \" \") {\n              e.preventDefault()\n            }\n          }}\n          onChange={(e) =>\n            updateField(\"symbol\", e.target.value.replace(/\\s/g, \"\"))\n          }\n        />\n      </div>\n\n      {/* Icon upload */}\n      <div className=\"grid 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          {data.iconPreviewUrl ? (\n            <div className=\"relative size-20 flex-shrink-0 overflow-hidden rounded-lg border bg-muted\">\n              <img\n                src={data.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-6\"\n                onClick={handleIconRemove}\n                aria-label=\"Remove icon\"\n              >\n                <X data-icon=\"inline-start\" />\n              </Button>\n            </div>\n          ) : (\n            <div className=\"flex size-20 flex-shrink-0 items-center justify-center rounded-lg border border-dashed bg-muted/50\">\n              <ImageIcon className=\"size-8 text-muted-foreground/50\" />\n            </div>\n          )}\n\n          <div className=\"flex-1\">\n            <Label\n              htmlFor=\"bsv21-icon-input\"\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              {data.icon ? \"Change Icon\" : \"Select Image\"}\n            </Label>\n            <input\n              type=\"file\"\n              id=\"bsv21-icon-input\"\n              accept=\"image/*\"\n              className=\"sr-only\"\n              onChange={handleIconSelect}\n            />\n          </div>\n        </div>\n      </div>\n\n      {/* Max Supply */}\n      <div className=\"grid gap-2\">\n        <div className=\"flex items-center justify-between\">\n          <Label htmlFor=\"bsv21-max\">Max Supply</Label>\n          <span className=\"text-xs text-muted-foreground\">Whole tokens</span>\n        </div>\n        <Input\n          id=\"bsv21-max\"\n          type=\"text\"\n          inputMode=\"numeric\"\n          pattern=\"[0-9]*\"\n          placeholder=\"21000000\"\n          value={data.maxSupply}\n          onChange={(e) =>\n            updateField(\"maxSupply\", e.target.value.replace(/[^0-9]/g, \"\"))\n          }\n        />\n      </div>\n\n      {/* Decimals */}\n      <div className=\"grid gap-2\">\n        <div className=\"flex items-center justify-between\">\n          <Label htmlFor=\"bsv21-decimals\">Decimal Precision</Label>\n          <span className=\"text-xs text-muted-foreground\">Default: 8</span>\n        </div>\n        <Input\n          id=\"bsv21-decimals\"\n          type=\"text\"\n          inputMode=\"numeric\"\n          pattern=\"[0-9]*\"\n          min={0}\n          max={18}\n          placeholder=\"8\"\n          value={data.decimals}\n          onChange={(e) =>\n            updateField(\"decimals\", e.target.value.replace(/[^0-9]/g, \"\"))\n          }\n        />\n      </div>\n\n      {/* Info banner */}\n      <Card className=\"bg-muted/50\">\n        <CardContent className=\"p-3 text-sm text-muted-foreground\">\n          BSV21 deployments are indexed immediately. A listing fee may be required\n          before it appears in some marketplace interfaces.\n        </CardContent>\n      </Card>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "~/components/blocks/inscribe-file/bsv21-form.tsx"
    }
  ],
  "type": "registry:block"
}