{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "create-listing",
  "title": "Create Listing",
  "author": "Satchmo",
  "description": "Dialog-based ordinal listing component for selling NFTs on the global orderbook via @1sat/actions listOrdinal",
  "dependencies": [
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "badge",
    "button",
    "card",
    "dialog",
    "input",
    "label"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/create-listing/index.tsx",
      "content": "\"use client\"\n\nimport { type VariantProps } from \"class-variance-authority\"\nimport { CreateListingUI, createListingTriggerVariants } from \"./ui\"\nimport {\n  useCreateListing,\n  type OrdinalItem,\n  type ListOrdinalParams,\n  type ListOrdinalResult,\n  type UseCreateListingReturn,\n  type UseCreateListingOptions,\n} from \"./use-create-listing\"\n\n// ---------------------------------------------------------------------------\n// Re-exports\n// ---------------------------------------------------------------------------\n\nexport {\n  CreateListingUI,\n  createListingTriggerVariants,\n  type CreateListingUIProps,\n} from \"./ui\"\nexport {\n  useCreateListing,\n  type OrdinalItem,\n  type ListOrdinalParams,\n  type ListOrdinalResult,\n  type UseCreateListingReturn,\n  type UseCreateListingOptions,\n} from \"./use-create-listing\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface CreateListingProps\n  extends VariantProps<typeof createListingTriggerVariants> {\n  /** The ordinal to list for sale */\n  ordinal: OrdinalItem\n  /** Callback to execute the listing action */\n  onList: (params: ListOrdinalParams) => Promise<ListOrdinalResult>\n  /** Callback on successful listing */\n  onListed?: (result: ListOrdinalResult) => void\n  /** Callback on error */\n  onError?: (error: Error) => void\n  /** Default payout address */\n  defaultPayAddress?: string\n  /** Text for the trigger button */\n  triggerLabel?: string\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// Composed component\n// ---------------------------------------------------------------------------\n\n/**\n * A dialog-based ordinal listing component for selling NFTs on the global\n * orderbook. Renders a trigger button that opens a dialog with ordinal\n * preview, price input, payout address, and confirm action.\n *\n * @example\n * ```tsx\n * import { CreateListing } from \"@/components/blocks/create-listing\"\n *\n * <CreateListing\n *   ordinal={{ outpoint: \"abc...def.0\", name: \"My NFT\" }}\n *   onList={async (params) => {\n *     return await listOrdinal.execute(ctx, {\n *       ordinal: walletOutput,\n *       price: params.price,\n *       payAddress: params.payAddress,\n *     })\n *   }}\n *   defaultPayAddress=\"1A1z...\"\n * />\n * ```\n */\nexport function CreateListing({\n  ordinal,\n  onList,\n  onListed,\n  onError,\n  defaultPayAddress = \"\",\n  triggerLabel = \"List for Sale\",\n  variant = \"default\",\n  className,\n  onExternalLink,\n}: CreateListingProps) {\n  const hook = useCreateListing({\n    ordinal,\n    onList,\n    onListed,\n    onError,\n    defaultPayAddress,\n  })\n\n  return (\n    <CreateListingUI\n      ordinal={ordinal}\n      triggerLabel={triggerLabel}\n      variant={variant}\n      className={className}\n      open={hook.open}\n      onOpenChange={hook.handleOpenChange}\n      priceInput={hook.priceInput}\n      onPriceInputChange={hook.setPriceInput}\n      payAddress={hook.payAddress}\n      onPayAddressChange={hook.setPayAddress}\n      isListing={hook.isListing}\n      result={hook.result}\n      error={hook.error}\n      priceSats={hook.priceSats}\n      validationError={hook.validationError}\n      canSubmit={hook.canSubmit}\n      onList={hook.handleList}\n      onExternalLink={onExternalLink}\n    />\n  )\n}\n",
      "type": "registry:block",
      "target": "~/components/blocks/create-listing/index.tsx"
    },
    {
      "path": "registry/new-york/blocks/create-listing/ui.tsx",
      "content": "\"use client\"\n\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport {\n  AlertCircle,\n  CheckCircle2,\n  ExternalLink,\n  Loader2,\n  Tag,\n} from \"lucide-react\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\nimport { Card, CardContent } from \"@/components/ui/card\"\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@/components/ui/dialog\"\nimport { Input } from \"@/components/ui/input\"\nimport { Label } from \"@/components/ui/label\"\nimport { cn } from \"@/lib/utils\"\nimport type { OrdinalItem, ListOrdinalResult } from \"./use-create-listing\"\n\n// ---------------------------------------------------------------------------\n// Variant definitions\n// ---------------------------------------------------------------------------\n\nexport const createListingTriggerVariants = cva(\n  \"inline-flex items-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"rounded-md border border-input bg-background text-foreground shadow-sm hover:bg-accent hover:text-accent-foreground px-4 py-2 text-sm\",\n        primary:\n          \"rounded-md bg-primary text-primary-foreground shadow hover:bg-primary/90 px-4 py-2 text-sm\",\n        ghost:\n          \"rounded-md text-foreground hover:bg-accent hover:text-accent-foreground px-4 py-2 text-sm\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n)\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface CreateListingUIProps\n  extends VariantProps<typeof createListingTriggerVariants> {\n  /** Optional CSS class name */\n  className?: string\n  /** Text for the trigger button */\n  triggerLabel?: string\n  /** The ordinal to list for sale */\n  ordinal: OrdinalItem\n  /** Whether the dialog is open */\n  open: boolean\n  /** Handle dialog open/close */\n  onOpenChange: (nextOpen: boolean) => void\n  /** Current price input string */\n  priceInput: string\n  /** Set the price input */\n  onPriceInputChange: (value: string) => void\n  /** Current payout address */\n  payAddress: string\n  /** Set the payout address */\n  onPayAddressChange: (value: string) => void\n  /** Whether a listing is in progress */\n  isListing: boolean\n  /** Listing result */\n  result: ListOrdinalResult | null\n  /** Error message */\n  error: string | null\n  /** Parsed price in satoshis */\n  priceSats: number\n  /** Validation error message */\n  validationError: string | null\n  /** Whether the form can be submitted */\n  canSubmit: boolean\n  /** Execute the listing */\n  onList: () => void\n  /** Callback to handle external links (e.g. open in system browser from a WebView) */\n  onExternalLink?: (url: string) => void\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst ORDFS_CONTENT_URL = \"https://ordfs.network/content\"\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction satsToBsv(sats: number): string {\n  return (sats / 100000000).toFixed(8)\n}\n\nfunction formatSats(sats: number): string {\n  if (sats >= 100000000) {\n    return `${satsToBsv(sats)} BSV`\n  }\n  return `${sats.toLocaleString()} sats`\n}\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\nexport function CreateListingUI({\n  ordinal,\n  triggerLabel = \"List for Sale\",\n  variant = \"default\",\n  className,\n  open,\n  onOpenChange,\n  priceInput,\n  onPriceInputChange,\n  payAddress,\n  onPayAddressChange,\n  isListing,\n  result,\n  error,\n  priceSats,\n  validationError,\n  canSubmit,\n  onList,\n  onExternalLink,\n}: CreateListingUIProps) {\n  const thumbnailUrl = `${ORDFS_CONTENT_URL}/${ordinal.origin ?? ordinal.outpoint}`\n\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogTrigger asChild>\n        <button\n          type=\"button\"\n          className={cn(createListingTriggerVariants({ variant }), className)}\n        >\n          <Tag className=\"size-4\" />\n          {triggerLabel}\n        </button>\n      </DialogTrigger>\n\n      <DialogContent className=\"sm:max-w-md\">\n        <DialogHeader>\n          <DialogTitle>List Ordinal for Sale</DialogTitle>\n          <DialogDescription>\n            Set a price and payout address to list this ordinal on the global\n            orderbook.\n          </DialogDescription>\n        </DialogHeader>\n\n        <div className=\"flex flex-col gap-6 py-4\">\n          {/* Ordinal preview */}\n          <Card className=\"bg-muted/50\">\n            <CardContent className=\"flex items-center gap-4 p-3\">\n              <div className=\"size-16 flex-shrink-0 overflow-hidden rounded-md border bg-muted\">\n                <img\n                  src={thumbnailUrl}\n                  alt={ordinal.name ?? \"Ordinal\"}\n                  className=\"h-full w-full object-cover\"\n                  onError={(e) => {\n                    e.currentTarget.style.display = \"none\"\n                  }}\n                />\n              </div>\n              <div className=\"min-w-0 flex-1\">\n                <p className=\"truncate text-sm font-medium\">\n                  {ordinal.name ?? \"Unnamed Ordinal\"}\n                </p>\n                <p className=\"truncate text-xs text-muted-foreground font-mono\">\n                  {ordinal.outpoint}\n                </p>\n                {ordinal.contentType && (\n                  <Badge variant=\"outline\" className=\"mt-1 text-xs\">\n                    {ordinal.contentType}\n                  </Badge>\n                )}\n              </div>\n            </CardContent>\n          </Card>\n\n          {/* Price input */}\n          <div className=\"flex flex-col gap-2\">\n            <Label htmlFor=\"listing-price\">Price (satoshis)</Label>\n            <Input\n              id=\"listing-price\"\n              type=\"text\"\n              inputMode=\"numeric\"\n              pattern=\"[0-9]*\"\n              placeholder=\"e.g. 100000\"\n              value={priceInput}\n              onChange={(e) =>\n                onPriceInputChange(e.target.value.replace(/[^0-9]/g, \"\"))\n              }\n              disabled={isListing}\n              aria-describedby=\"price-display\"\n            />\n            {priceSats > 0 && (\n              <Badge\n                variant=\"secondary\"\n                id=\"price-display\"\n                className=\"w-fit text-xs\"\n              >\n                {formatSats(priceSats)}\n              </Badge>\n            )}\n          </div>\n\n          {/* Payout address */}\n          <div className=\"flex flex-col gap-2\">\n            <Label htmlFor=\"listing-pay-address\">Payout Address</Label>\n            <Input\n              id=\"listing-pay-address\"\n              type=\"text\"\n              placeholder=\"1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa\"\n              value={payAddress}\n              onChange={(e) => onPayAddressChange(e.target.value)}\n              disabled={isListing}\n              className=\"font-mono text-sm\"\n            />\n            <p className=\"text-xs text-muted-foreground\">\n              BSV address that will receive payment when this ordinal is\n              purchased.\n            </p>\n          </div>\n\n          {/* Validation error */}\n          {validationError && (\n            <p className=\"text-sm text-destructive\" role=\"alert\">\n              {validationError}\n            </p>\n          )}\n\n          {/* Success */}\n          {result?.txid && (\n            <div className=\"flex items-start gap-3 rounded-md border border-primary/20 bg-primary/5 p-3\">\n              <CheckCircle2 className=\"mt-0.5 size-4 flex-shrink-0 text-primary\" />\n              <div className=\"min-w-0 flex flex-col gap-1\">\n                <p className=\"text-sm font-medium\">Listed successfully</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 */}\n          {error && (\n            <div className=\"flex items-start gap-3 rounded-md border border-destructive/20 bg-destructive/5 p-3\">\n              <AlertCircle className=\"mt-0.5 size-4 flex-shrink-0 text-destructive\" />\n              <div className=\"flex flex-col gap-1\">\n                <p className=\"text-sm font-medium\">Listing failed</p>\n                <p className=\"text-xs text-muted-foreground\">{error}</p>\n              </div>\n            </div>\n          )}\n        </div>\n\n        <DialogFooter>\n          <Button\n            className=\"w-full\"\n            onClick={onList}\n            disabled={!canSubmit || isListing}\n            aria-busy={isListing}\n          >\n            {isListing ? (\n              <>\n                <Loader2 className=\"animate-spin\" data-icon=\"inline-start\" />\n                Creating Listing...\n              </>\n            ) : (\n              `List for ${priceSats > 0 ? formatSats(priceSats) : \"...\"}`\n            )}\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  )\n}\n",
      "type": "registry:component",
      "target": "~/components/blocks/create-listing/ui.tsx"
    },
    {
      "path": "registry/new-york/blocks/create-listing/use-create-listing.ts",
      "content": "import { useCallback, useMemo, useState } from \"react\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Represents an ordinal NFT to be listed */\nexport interface OrdinalItem {\n  /** Outpoint in txid.vout format */\n  outpoint: string\n  /** Display name of the ordinal */\n  name?: string\n  /** Content type (MIME type) */\n  contentType?: string\n  /** Origin outpoint for collection grouping */\n  origin?: string\n}\n\nexport interface ListOrdinalParams {\n  /** The ordinal to list */\n  ordinal: OrdinalItem\n  /** Price in satoshis */\n  price: number\n  /** Address that receives payment on purchase */\n  payAddress: string\n}\n\nexport interface ListOrdinalResult {\n  /** Transaction ID of the listing */\n  txid?: string\n  /** Raw transaction hex */\n  rawtx?: string\n  /** Error message if listing failed */\n  error?: string\n}\n\nexport interface UseCreateListingOptions {\n  /** The ordinal to list for sale */\n  ordinal: OrdinalItem\n  /** Callback to execute the listing action */\n  onList: (params: ListOrdinalParams) => Promise<ListOrdinalResult>\n  /** Callback on successful listing */\n  onListed?: (result: ListOrdinalResult) => void\n  /** Callback on error */\n  onError?: (error: Error) => void\n  /** Default payout address */\n  defaultPayAddress?: string\n}\n\nexport interface UseCreateListingReturn {\n  /** Whether the dialog is open */\n  open: boolean\n  /** Handle dialog open/close */\n  handleOpenChange: (nextOpen: boolean) => void\n  /** Current price input string */\n  priceInput: string\n  /** Set the price input */\n  setPriceInput: (value: string) => void\n  /** Current payout address */\n  payAddress: string\n  /** Set the payout address */\n  setPayAddress: (value: string) => void\n  /** Whether a listing is in progress */\n  isListing: boolean\n  /** Listing result */\n  result: ListOrdinalResult | null\n  /** Error message */\n  error: string | null\n  /** Parsed price in satoshis */\n  priceSats: number\n  /** Validation error message */\n  validationError: string | null\n  /** Whether the form can be submitted */\n  canSubmit: boolean\n  /** Execute the listing */\n  handleList: () => Promise<void>\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst MIN_PRICE_SATS = 1\nconst MAX_PRICE_SATS = 2100000000000000\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\nexport function useCreateListing({\n  ordinal,\n  onList,\n  onListed,\n  onError,\n  defaultPayAddress = \"\",\n}: UseCreateListingOptions): UseCreateListingReturn {\n  const [open, setOpen] = useState(false)\n  const [priceInput, setPriceInput] = useState(\"\")\n  const [payAddress, setPayAddress] = useState(defaultPayAddress)\n  const [isListing, setIsListing] = useState(false)\n  const [result, setResult] = useState<ListOrdinalResult | null>(null)\n  const [error, setError] = useState<string | null>(null)\n\n  const priceSats = useMemo(() => {\n    const parsed = Number.parseInt(priceInput, 10)\n    if (Number.isNaN(parsed) || parsed < MIN_PRICE_SATS) return 0\n    if (parsed > MAX_PRICE_SATS) return MAX_PRICE_SATS\n    return parsed\n  }, [priceInput])\n\n  const validationError = useMemo(() => {\n    if (!priceInput) return null\n    if (priceSats < MIN_PRICE_SATS) return \"Price must be at least 1 satoshi\"\n    if (priceSats > MAX_PRICE_SATS) return \"Price exceeds maximum\"\n    if (!payAddress.trim()) return \"Payout address is required\"\n    return null\n  }, [priceInput, priceSats, payAddress])\n\n  const canSubmit =\n    priceSats >= MIN_PRICE_SATS &&\n    payAddress.trim().length > 0 &&\n    !validationError\n\n  const handleList = useCallback(async () => {\n    if (!canSubmit) return\n\n    setIsListing(true)\n    setError(null)\n    setResult(null)\n\n    try {\n      const listResult = await onList({\n        ordinal,\n        price: priceSats,\n        payAddress: payAddress.trim(),\n      })\n\n      if (listResult.error) {\n        setError(listResult.error)\n        onError?.(new Error(listResult.error))\n      } else {\n        setResult(listResult)\n        onListed?.(listResult)\n      }\n    } catch (err) {\n      const msg =\n        err instanceof Error ? err.message : \"Failed to create listing\"\n      setError(msg)\n      onError?.(err instanceof Error ? err : new Error(msg))\n    } finally {\n      setIsListing(false)\n    }\n  }, [canSubmit, ordinal, priceSats, payAddress, onList, onListed, onError])\n\n  const handleOpenChange = useCallback(\n    (nextOpen: boolean) => {\n      if (!isListing) {\n        setOpen(nextOpen)\n        if (!nextOpen) {\n          // Reset form state on close\n          setPriceInput(\"\")\n          setPayAddress(defaultPayAddress)\n          setResult(null)\n          setError(null)\n        }\n      }\n    },\n    [isListing, defaultPayAddress],\n  )\n\n  return {\n    open,\n    handleOpenChange,\n    priceInput,\n    setPriceInput,\n    payAddress,\n    setPayAddress,\n    isListing,\n    result,\n    error,\n    priceSats,\n    validationError,\n    canSubmit,\n    handleList,\n  }\n}\n",
      "type": "registry:component",
      "target": "~/components/blocks/create-listing/use-create-listing.ts"
    }
  ],
  "type": "registry:block"
}