{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "buy-listing",
  "title": "Buy Listing",
  "author": "Satchmo",
  "description": "Card component for purchasing ordinal NFTs from the global orderbook via @1sat/actions purchaseOrdinal",
  "dependencies": [
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "badge",
    "button",
    "card",
    "separator"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/buy-listing/index.tsx",
      "content": "\"use client\"\n\nimport { type VariantProps } from \"class-variance-authority\"\nimport { BuyListingUI, buyListingVariants } from \"./ui\"\nimport {\n  useBuyListing,\n  type PurchaseOrdinalParams,\n  type PurchaseOrdinalResult,\n  type UseBuyListingReturn,\n  type UseBuyListingOptions,\n} from \"./use-buy-listing\"\n\n// ---------------------------------------------------------------------------\n// Re-exports\n// ---------------------------------------------------------------------------\n\nexport {\n  BuyListingUI,\n  buyListingVariants,\n  type BuyListingUIProps,\n} from \"./ui\"\nexport {\n  useBuyListing,\n  type PurchaseOrdinalParams,\n  type PurchaseOrdinalResult,\n  type UseBuyListingReturn,\n  type UseBuyListingOptions,\n} from \"./use-buy-listing\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface BuyListingProps\n  extends VariantProps<typeof buyListingVariants> {\n  /** Outpoint of the listing (txid.vout format) */\n  outpoint: string\n  /** Price in satoshis */\n  price: number\n  /** Seller display name or address */\n  seller?: string\n  /** Name of the ordinal */\n  name?: string\n  /** Content type (MIME type) for the thumbnail */\n  contentType?: string\n  /** Origin outpoint for thumbnail resolution */\n  origin?: string\n  /** Callback to execute the purchase action */\n  onPurchase: (params: PurchaseOrdinalParams) => Promise<PurchaseOrdinalResult>\n  /** Callback on successful purchase */\n  onPurchased?: (result: PurchaseOrdinalResult) => void\n  /** Callback on error */\n  onError?: (error: Error) => void\n  /** Marketplace address for fees */\n  marketplaceAddress?: string\n  /** Marketplace fee rate 0-1 */\n  marketplaceRate?: number\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// Constants\n// ---------------------------------------------------------------------------\n\nconst ORDFS_CONTENT_URL = \"https://ordfs.network/content\"\n\n// ---------------------------------------------------------------------------\n// Composed component\n// ---------------------------------------------------------------------------\n\n/**\n * A card component for purchasing ordinal NFTs from the global orderbook.\n *\n * Displays a thumbnail from ORDFS, price, optional seller info, marketplace\n * fee breakdown, and a \"Buy Now\" action. Uses the `purchaseOrdinal` action\n * from `@1sat/actions` via the `onPurchase` callback.\n *\n * @example\n * ```tsx\n * import { BuyListing } from \"@/components/blocks/buy-listing\"\n *\n * <BuyListing\n *   outpoint=\"abc123...def.0\"\n *   price={50000}\n *   name=\"Rare Pepe #42\"\n *   seller=\"1A1z...\"\n *   onPurchase={async (params) => {\n *     return await purchaseOrdinal.execute(ctx, params)\n *   }}\n * />\n * ```\n */\nexport function BuyListing({\n  outpoint,\n  price,\n  seller,\n  name,\n  contentType,\n  origin,\n  onPurchase,\n  onPurchased,\n  onError,\n  marketplaceAddress,\n  marketplaceRate,\n  size = \"default\",\n  className,\n  onExternalLink,\n}: BuyListingProps) {\n  const hook = useBuyListing({\n    outpoint,\n    price,\n    onPurchase,\n    onPurchased,\n    onError,\n    marketplaceAddress,\n    marketplaceRate,\n  })\n\n  const thumbnailUrl = `${ORDFS_CONTENT_URL}/${origin ?? outpoint}`\n\n  return (\n    <BuyListingUI\n      size={size}\n      className={className}\n      name={name}\n      seller={seller}\n      contentType={contentType}\n      price={price}\n      thumbnailUrl={thumbnailUrl}\n      imgError={hook.imgError}\n      onImgError={() => hook.setImgError(true)}\n      marketFee={hook.marketFee}\n      totalCost={hook.totalCost}\n      isPurchasing={hook.isPurchasing}\n      result={hook.result}\n      error={hook.error}\n      onPurchase={hook.handlePurchase}\n      onExternalLink={onExternalLink}\n    />\n  )\n}\n",
      "type": "registry:block",
      "target": "~/components/blocks/buy-listing/index.tsx"
    },
    {
      "path": "registry/new-york/blocks/buy-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  ShoppingCart,\n} from \"lucide-react\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Card,\n  CardContent,\n  CardFooter,\n  CardHeader,\n} from \"@/components/ui/card\"\nimport { Separator } from \"@/components/ui/separator\"\nimport { cn } from \"@/lib/utils\"\nimport type { PurchaseOrdinalResult } from \"./use-buy-listing\"\n\n// ---------------------------------------------------------------------------\n// Variant definitions\n// ---------------------------------------------------------------------------\n\nexport const buyListingVariants = cva(\n  \"group overflow-hidden transition-shadow hover:shadow-lg\",\n  {\n    variants: {\n      size: {\n        sm: \"max-w-[200px]\",\n        default: \"\",\n        lg: \"max-w-[400px]\",\n      },\n    },\n    defaultVariants: {\n      size: \"default\",\n    },\n  },\n)\n\nconst thumbnailVariants = cva(\n  \"relative overflow-hidden bg-muted\",\n  {\n    variants: {\n      size: {\n        sm: \"aspect-square\",\n        default: \"aspect-square\",\n        lg: \"aspect-[4/3]\",\n      },\n    },\n    defaultVariants: {\n      size: \"default\",\n    },\n  },\n)\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface BuyListingUIProps\n  extends VariantProps<typeof buyListingVariants> {\n  /** Optional CSS class name */\n  className?: string\n  /** Name of the ordinal */\n  name?: string\n  /** Seller display name or address */\n  seller?: string\n  /** Content type (MIME type) for the thumbnail */\n  contentType?: string\n  /** Price in satoshis */\n  price: number\n  /** Thumbnail URL */\n  thumbnailUrl: string\n  /** Whether the thumbnail image failed to load */\n  imgError: boolean\n  /** Handle thumbnail image error */\n  onImgError: () => void\n  /** Marketplace fee in satoshis */\n  marketFee: number\n  /** Total cost in satoshis */\n  totalCost: number\n  /** Whether a purchase is in progress */\n  isPurchasing: boolean\n  /** Purchase result */\n  result: PurchaseOrdinalResult | null\n  /** Error message */\n  error: string | null\n  /** Execute the purchase */\n  onPurchase: () => 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// Helpers\n// ---------------------------------------------------------------------------\n\nfunction satsToBsv(sats: number): string {\n  return (sats / 100000000).toFixed(8)\n}\n\nfunction formatPrice(sats: number): string {\n  if (sats >= 100000000) {\n    return `${satsToBsv(sats)} BSV`\n  }\n  if (sats >= 1000000) {\n    return `${(sats / 1000000).toFixed(2)}M sats`\n  }\n  if (sats >= 1000) {\n    return `${(sats / 1000).toFixed(1)}K sats`\n  }\n  return `${sats.toLocaleString()} sats`\n}\n\nfunction truncateAddress(address: string, chars = 6): string {\n  if (address.length <= chars * 2 + 3) return address\n  return `${address.slice(0, chars)}...${address.slice(-chars)}`\n}\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\nexport function BuyListingUI({\n  size = \"default\",\n  className,\n  name,\n  seller,\n  contentType,\n  price,\n  thumbnailUrl,\n  imgError,\n  onImgError,\n  marketFee,\n  totalCost,\n  isPurchasing,\n  result,\n  error,\n  onPurchase,\n  onExternalLink,\n}: BuyListingUIProps) {\n  return (\n    <Card className={cn(buyListingVariants({ size }), className)}>\n      {/* Thumbnail */}\n      <div className={thumbnailVariants({ size })}>\n        {!imgError ? (\n          <img\n            src={thumbnailUrl}\n            alt={name ?? \"Ordinal listing\"}\n            className=\"h-full w-full object-cover transition-transform duration-300 group-hover:scale-105\"\n            onError={onImgError}\n          />\n        ) : (\n          <div className=\"flex h-full w-full items-center justify-center\">\n            <div className=\"text-center text-muted-foreground\">\n              <ShoppingCart className=\"mx-auto size-8 opacity-30\" />\n              <p className=\"mt-2 text-xs\">\n                {contentType ?? \"Unknown type\"}\n              </p>\n            </div>\n          </div>\n        )}\n\n        {/* Price badge */}\n        <Badge\n          variant=\"secondary\"\n          className=\"absolute bottom-2 right-2 bg-background/90 backdrop-blur-sm\"\n        >\n          {formatPrice(price)}\n        </Badge>\n      </div>\n\n      <CardHeader className=\"p-4 pb-2\">\n        <div className=\"flex flex-col gap-1\">\n          <h3 className=\"truncate text-sm font-semibold leading-none\">\n            {name ?? \"Unnamed Ordinal\"}\n          </h3>\n          {seller && (\n            <p className=\"text-xs text-muted-foreground\">\n              Seller: {truncateAddress(seller)}\n            </p>\n          )}\n        </div>\n      </CardHeader>\n\n      <CardContent className=\"flex flex-col gap-2 px-4 pb-2\">\n        {/* Cost breakdown */}\n        <div className=\"flex flex-col gap-1 text-xs text-muted-foreground\">\n          <div className=\"flex justify-between\">\n            <span>Price</span>\n            <span>{price.toLocaleString()} sats</span>\n          </div>\n          {marketFee > 0 && (\n            <div className=\"flex justify-between\">\n              <span>Marketplace fee</span>\n              <span>{marketFee.toLocaleString()} sats</span>\n            </div>\n          )}\n          {marketFee > 0 && (\n            <>\n              <Separator />\n              <div className=\"flex justify-between pt-1 font-medium text-foreground\">\n                <span>Total</span>\n                <span>{totalCost.toLocaleString()} sats</span>\n              </div>\n            </>\n          )}\n        </div>\n\n        {/* Success */}\n        {result?.txid && (\n          <div className=\"flex items-start gap-2 rounded-md border border-primary/20 bg-primary/5 p-2\">\n            <CheckCircle2 className=\"mt-0.5 size-4 flex-shrink-0 text-primary\" />\n            <div className=\"min-w-0 flex-1\">\n              <p className=\"text-xs font-medium\">Purchased</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 text-[10px] transition-colors hover:text-foreground\"\n                >\n                  <Badge variant=\"outline\" className=\"max-w-full truncate text-[10px] font-mono\">\n                    {result.txid}\n                  </Badge>\n                  <ExternalLink className=\"size-2.5 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 text-[10px] transition-colors hover:text-foreground\"\n                >\n                  <Badge variant=\"outline\" className=\"max-w-full truncate text-[10px] font-mono\">\n                    {result.txid}\n                  </Badge>\n                  <ExternalLink className=\"size-2.5 flex-shrink-0\" />\n                </a>\n              )}\n            </div>\n          </div>\n        )}\n\n        {/* Error */}\n        {error && (\n          <div className=\"flex items-start gap-2 rounded-md border border-destructive/20 bg-destructive/5 p-2\">\n            <AlertCircle className=\"mt-0.5 size-4 flex-shrink-0 text-destructive\" />\n            <p className=\"text-xs text-muted-foreground\">{error}</p>\n          </div>\n        )}\n      </CardContent>\n\n      <CardFooter className=\"p-4 pt-2\">\n        <Button\n          className=\"w-full gap-2\"\n          onClick={onPurchase}\n          disabled={isPurchasing || !!result?.txid}\n          aria-busy={isPurchasing}\n        >\n          {isPurchasing ? (\n            <>\n              <Loader2 className=\"animate-spin\" data-icon=\"inline-start\" />\n              Purchasing...\n            </>\n          ) : result?.txid ? (\n            <>\n              <CheckCircle2 data-icon=\"inline-start\" />\n              Purchased\n            </>\n          ) : (\n            <>\n              <ShoppingCart data-icon=\"inline-start\" />\n              Buy Now\n            </>\n          )}\n        </Button>\n      </CardFooter>\n    </Card>\n  )\n}\n",
      "type": "registry:component",
      "target": "~/components/blocks/buy-listing/ui.tsx"
    },
    {
      "path": "registry/new-york/blocks/buy-listing/use-buy-listing.ts",
      "content": "import { useCallback, useState } from \"react\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface PurchaseOrdinalParams {\n  /** Outpoint of the listing to purchase */\n  outpoint: string\n  /** Marketplace address for fees (optional) */\n  marketplaceAddress?: string\n  /** Marketplace fee rate 0-1 (optional) */\n  marketplaceRate?: number\n}\n\nexport interface PurchaseOrdinalResult {\n  /** Transaction ID of the purchase */\n  txid?: string\n  /** Raw transaction hex */\n  rawtx?: string\n  /** Error message if purchase failed */\n  error?: string\n}\n\nexport interface UseBuyListingOptions {\n  /** Outpoint of the listing (txid.vout format) */\n  outpoint: string\n  /** Price in satoshis */\n  price: number\n  /** Callback to execute the purchase action */\n  onPurchase: (params: PurchaseOrdinalParams) => Promise<PurchaseOrdinalResult>\n  /** Callback on successful purchase */\n  onPurchased?: (result: PurchaseOrdinalResult) => void\n  /** Callback on error */\n  onError?: (error: Error) => void\n  /** Marketplace address for fees */\n  marketplaceAddress?: string\n  /** Marketplace fee rate 0-1 */\n  marketplaceRate?: number\n}\n\nexport interface UseBuyListingReturn {\n  /** Whether a purchase is in progress */\n  isPurchasing: boolean\n  /** Purchase result */\n  result: PurchaseOrdinalResult | null\n  /** Error message */\n  error: string | null\n  /** Whether the thumbnail image failed to load */\n  imgError: boolean\n  /** Mark the thumbnail as failed */\n  setImgError: (failed: boolean) => void\n  /** Marketplace fee in satoshis */\n  marketFee: number\n  /** Total cost in satoshis (price + marketplace fee) */\n  totalCost: number\n  /** Execute the purchase */\n  handlePurchase: () => Promise<void>\n}\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\nexport function useBuyListing({\n  outpoint,\n  price,\n  onPurchase,\n  onPurchased,\n  onError,\n  marketplaceAddress,\n  marketplaceRate,\n}: UseBuyListingOptions): UseBuyListingReturn {\n  const [isPurchasing, setIsPurchasing] = useState(false)\n  const [result, setResult] = useState<PurchaseOrdinalResult | null>(null)\n  const [error, setError] = useState<string | null>(null)\n  const [imgError, setImgError] = useState(false)\n\n  const marketFee =\n    marketplaceAddress && marketplaceRate\n      ? Math.ceil(price * marketplaceRate)\n      : 0\n  const totalCost = price + marketFee\n\n  const handlePurchase = useCallback(async () => {\n    setIsPurchasing(true)\n    setError(null)\n    setResult(null)\n\n    try {\n      const purchaseResult = await onPurchase({\n        outpoint,\n        marketplaceAddress,\n        marketplaceRate,\n      })\n\n      if (purchaseResult.error) {\n        setError(purchaseResult.error)\n        onError?.(new Error(purchaseResult.error))\n      } else {\n        setResult(purchaseResult)\n        onPurchased?.(purchaseResult)\n      }\n    } catch (err) {\n      const msg =\n        err instanceof Error ? err.message : \"Purchase failed\"\n      setError(msg)\n      onError?.(err instanceof Error ? err : new Error(msg))\n    } finally {\n      setIsPurchasing(false)\n    }\n  }, [\n    outpoint,\n    marketplaceAddress,\n    marketplaceRate,\n    onPurchase,\n    onPurchased,\n    onError,\n  ])\n\n  return {\n    isPurchasing,\n    result,\n    error,\n    imgError,\n    setImgError,\n    marketFee,\n    totalCost,\n    handlePurchase,\n  }\n}\n",
      "type": "registry:component",
      "target": "~/components/blocks/buy-listing/use-buy-listing.ts"
    }
  ],
  "type": "registry:block"
}