{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "market-grid",
  "title": "Market Grid",
  "author": "Satchmo",
  "description": "Responsive grid of ordinal NFT listings from the global orderbook with ORDFS thumbnails, price badges, seller info, buy actions, pagination, and skeleton loading",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "card",
    "badge",
    "skeleton",
    "avatar"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/market-grid/index.tsx",
      "content": "\"use client\"\n\nimport { MarketGridUI } from \"./market-grid-ui\"\nimport {\n  useMarketGrid,\n  type MarketListing,\n  type SortDirection,\n  type SortField,\n  type UseMarketGridOptions,\n  type UseMarketGridReturn,\n} from \"./use-market-grid\"\n\n// ---------------------------------------------------------------------------\n// Re-exports\n// ---------------------------------------------------------------------------\n\nexport { ListingCardUI, type ListingCardUIProps } from \"./listing-card-ui\"\nexport { MarketGridUI, type MarketGridUIProps } from \"./market-grid-ui\"\nexport {\n  useMarketGrid,\n  type MarketListing,\n  type OneSatTxo,\n  type SortDirection,\n  type SortField,\n  type UseMarketGridOptions,\n  type UseMarketGridReturn,\n} from \"./use-market-grid\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface MarketGridProps {\n  /** Base URL of the 1sat-stack API */\n  apiUrl?: string\n  /** Number of listings per page */\n  pageSize?: number\n  /** Sort field */\n  sortBy?: SortField\n  /** Sort direction */\n  sortDir?: SortDirection\n  /** Optional content type filter (e.g. \"image/png\") */\n  contentTypeFilter?: string\n  /** Callback when \"Buy\" is clicked on a listing */\n  onBuy: (outpoint: string, price: number) => void\n  /** Callback when a listing card is clicked (navigation) */\n  onListingClick?: (outpoint: string) => void\n  /** Callback to handle external links (e.g. open in system browser from a WebView) */\n  onExternalLink?: (url: string) => void\n  /** Number of skeleton cards to show during loading */\n  skeletonCount?: number\n  /** Optional CSS class name */\n  className?: string\n}\n\n// ---------------------------------------------------------------------------\n// Composed component\n// ---------------------------------------------------------------------------\n\n/**\n * A responsive grid of ordinal NFT listings from the global orderbook.\n *\n * Fetches active OrdLock listings from the 1sat-stack API and renders each\n * as a card with ORDFS thumbnail, price badge, seller avatar, and buy action.\n * Supports pagination, loading skeletons, error recovery, and empty states.\n *\n * @example\n * ```tsx\n * import { MarketGrid } from \"@/components/blocks/market-grid\"\n *\n * <MarketGrid\n *   onBuy={(outpoint, price) => {\n *     console.log(`Buy ${outpoint} for ${price} sats`)\n *   }}\n *   onListingClick={(outpoint) => {\n *     router.push(`/ordinal/${outpoint}`)\n *   }}\n * />\n * ```\n */\nexport function MarketGrid({\n  apiUrl,\n  pageSize,\n  sortBy,\n  sortDir,\n  contentTypeFilter,\n  onBuy,\n  onListingClick,\n  onExternalLink,\n  skeletonCount,\n  className,\n}: MarketGridProps) {\n  const grid = useMarketGrid({\n    apiUrl,\n    pageSize,\n    sortBy,\n    sortDir,\n    contentTypeFilter,\n  })\n\n  return (\n    <MarketGridUI\n      listings={grid.listings}\n      isLoading={grid.isLoading}\n      isLoadingMore={grid.isLoadingMore}\n      error={grid.error}\n      hasMore={grid.hasMore}\n      onLoadMore={grid.loadMore}\n      onRefresh={grid.refresh}\n      onBuy={onBuy}\n      onListingClick={onListingClick}\n      onExternalLink={onExternalLink}\n      skeletonCount={skeletonCount}\n      className={className}\n    />\n  )\n}\n",
      "type": "registry:block",
      "target": "~/components/blocks/market-grid/index.tsx"
    },
    {
      "path": "registry/new-york/blocks/market-grid/listing-card-ui.tsx",
      "content": "\"use client\"\n\nimport { useState } from \"react\"\nimport { ExternalLink, ShoppingCart } from \"lucide-react\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Card,\n  CardContent,\n  CardFooter,\n} from \"@/components/ui/card\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport {\n  Avatar,\n  AvatarFallback,\n} from \"@/components/ui/avatar\"\nimport { cn } from \"@/lib/utils\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ListingCardUIProps {\n  /** Outpoint of the listing (txid.vout) */\n  outpoint: string\n  /** Listing price in satoshis */\n  price: number\n  /** Seller address */\n  seller: string\n  /** Content type (MIME type) */\n  contentType: string\n  /** ORDFS thumbnail URL */\n  thumbnailUrl: string\n  /** Optional ordinal name */\n  name: string | null\n  /** Click on the \"Buy\" button */\n  onBuy: (outpoint: string, price: number) => void\n  /** Click on the card body to navigate */\n  onListingClick?: (outpoint: string) => 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 formatPrice(sats: number): string {\n  if (sats >= 100000000) {\n    return `${(sats / 100000000).toFixed(8)} 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 = 4): string {\n  if (address.length <= chars * 2 + 3) return address\n  return `${address.slice(0, chars)}...${address.slice(-chars)}`\n}\n\nfunction sellerInitials(address: string): string {\n  if (address.length < 2) return \"?\"\n  return address.slice(0, 2).toUpperCase()\n}\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\nexport function ListingCardUI({\n  outpoint,\n  price,\n  seller,\n  contentType,\n  thumbnailUrl,\n  name,\n  onBuy,\n  onListingClick,\n  onExternalLink,\n  className,\n}: ListingCardUIProps) {\n  const [imgLoaded, setImgLoaded] = useState(false)\n  const [imgError, setImgError] = useState(false)\n\n  const isImage = contentType.startsWith(\"image/\")\n\n  return (\n    <Card\n      className={cn(\n        \"group overflow-hidden transition-shadow hover:shadow-lg\",\n        onListingClick && \"cursor-pointer\",\n        className,\n      )}\n      onClick={() => onListingClick?.(outpoint)}\n      role={onListingClick ? \"button\" : undefined}\n      tabIndex={onListingClick ? 0 : undefined}\n      onKeyDown={(e) => {\n        if (onListingClick && (e.key === \"Enter\" || e.key === \" \")) {\n          e.preventDefault()\n          onListingClick(outpoint)\n        }\n      }}\n    >\n      {/* Thumbnail */}\n      <div className=\"relative aspect-square overflow-hidden bg-muted\">\n        {isImage && !imgError ? (\n          <>\n            {!imgLoaded && (\n              <Skeleton className=\"absolute inset-0 h-full w-full\" />\n            )}\n            <img\n              src={thumbnailUrl}\n              alt={name ?? \"Ordinal listing\"}\n              className={cn(\n                \"h-full w-full object-cover transition-transform duration-300 group-hover:scale-105\",\n                !imgLoaded && \"opacity-0\",\n              )}\n              onLoad={() => setImgLoaded(true)}\n              onError={() => setImgError(true)}\n            />\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 overlay */}\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      <CardContent className=\"flex flex-col gap-2 p-3 pb-2\">\n        {/* Name + external link */}\n        <div className=\"flex items-center gap-1\">\n          <h3 className=\"truncate text-sm font-semibold leading-none\">\n            {name ?? \"Unnamed Ordinal\"}\n          </h3>\n          {(() => {\n            const ordfsUrl = `https://ordfs.network/${outpoint}`\n            return onExternalLink ? (\n              <button\n                type=\"button\"\n                onClick={(e) => {\n                  e.stopPropagation()\n                  onExternalLink(ordfsUrl)\n                }}\n                className=\"flex-shrink-0 text-muted-foreground transition-colors hover:text-foreground\"\n                aria-label=\"View on ORDFS\"\n              >\n                <ExternalLink className=\"size-3\" />\n              </button>\n            ) : (\n              <a\n                href={ordfsUrl}\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n                onClick={(e) => e.stopPropagation()}\n                className=\"flex-shrink-0 text-muted-foreground transition-colors hover:text-foreground\"\n                aria-label=\"View on ORDFS\"\n              >\n                <ExternalLink className=\"size-3\" />\n              </a>\n            )\n          })()}\n        </div>\n\n        {/* Seller row */}\n        {seller && (\n          <div className=\"flex items-center gap-2\">\n            <Avatar className=\"size-5\">\n              <AvatarFallback className=\"text-[8px]\">\n                {sellerInitials(seller)}\n              </AvatarFallback>\n            </Avatar>\n            <span className=\"text-xs text-muted-foreground\">\n              {truncateAddress(seller)}\n            </span>\n          </div>\n        )}\n      </CardContent>\n\n      <CardFooter className=\"p-3 pt-0\">\n        <Button\n          className=\"w-full gap-2\"\n          size=\"sm\"\n          onClick={(e) => {\n            e.stopPropagation()\n            onBuy(outpoint, price)\n          }}\n          aria-label={`Buy ${name ?? \"ordinal\"} for ${formatPrice(price)}`}\n        >\n          <ShoppingCart data-icon=\"inline-start\" />\n          Buy\n        </Button>\n      </CardFooter>\n    </Card>\n  )\n}\n",
      "type": "registry:component",
      "target": "~/components/blocks/market-grid/listing-card-ui.tsx"
    },
    {
      "path": "registry/new-york/blocks/market-grid/market-grid-ui.tsx",
      "content": "\"use client\"\n\nimport { AlertCircle, Loader2, RefreshCw, Store } from \"lucide-react\"\nimport { Button } from \"@/components/ui/button\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport { Card, CardContent, CardFooter } from \"@/components/ui/card\"\nimport { cn } from \"@/lib/utils\"\nimport { ListingCardUI } from \"./listing-card-ui\"\nimport type { MarketListing } from \"./use-market-grid\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface MarketGridUIProps {\n  /** Listings to display */\n  listings: MarketListing[]\n  /** Whether the initial load is in progress */\n  isLoading: boolean\n  /** Whether more listings are loading */\n  isLoadingMore: boolean\n  /** Error message from the most recent fetch */\n  error: string | null\n  /** Whether there are more listings to load */\n  hasMore: boolean\n  /** Load next page */\n  onLoadMore: () => void\n  /** Re-fetch listings from the beginning */\n  onRefresh: () => void\n  /** Callback when \"Buy\" is clicked on a listing */\n  onBuy: (outpoint: string, price: number) => void\n  /** Callback when a listing card is clicked (navigation) */\n  onListingClick?: (outpoint: string) => void\n  /** Callback to handle external links (e.g. open in system browser from a WebView) */\n  onExternalLink?: (url: string) => void\n  /** Number of skeleton cards to show during loading */\n  skeletonCount?: number\n  /** Optional CSS class name */\n  className?: string\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst ORDFS_CONTENT_URL = \"https://ordfs.network/content\"\n\n// ---------------------------------------------------------------------------\n// Skeleton grid\n// ---------------------------------------------------------------------------\n\nfunction SkeletonCard() {\n  return (\n    <Card className=\"overflow-hidden\">\n      <Skeleton className=\"aspect-square w-full\" />\n      <CardContent className=\"flex flex-col gap-2 p-3 pb-2\">\n        <Skeleton className=\"h-4 w-3/4\" />\n        <div className=\"flex items-center gap-2\">\n          <Skeleton className=\"size-5 rounded-full\" />\n          <Skeleton className=\"h-3 w-20\" />\n        </div>\n      </CardContent>\n      <CardFooter className=\"p-3 pt-0\">\n        <Skeleton className=\"h-8 w-full\" />\n      </CardFooter>\n    </Card>\n  )\n}\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\nexport function MarketGridUI({\n  listings,\n  isLoading,\n  isLoadingMore,\n  error,\n  hasMore,\n  onLoadMore,\n  onRefresh,\n  onBuy,\n  onListingClick,\n  onExternalLink,\n  skeletonCount = 8,\n  className,\n}: MarketGridUIProps) {\n  // Loading state: grid of skeletons\n  if (isLoading) {\n    return (\n      <div className={cn(\"flex flex-col gap-6\", className)}>\n        <div className=\"grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4\">\n          {Array.from({ length: skeletonCount }, (_, i) => (\n            <SkeletonCard key={i} />\n          ))}\n        </div>\n      </div>\n    )\n  }\n\n  // Error state (no listings loaded)\n  if (error && listings.length === 0) {\n    return (\n      <div className={cn(\"flex flex-col gap-6\", className)}>\n        <div className=\"flex flex-col items-center justify-center gap-4 rounded-lg border border-dashed p-10\">\n          <AlertCircle className=\"size-10 text-destructive\" />\n          <div className=\"flex flex-col gap-1 text-center\">\n            <p className=\"text-sm font-medium\">Failed to load listings</p>\n            <p className=\"text-xs text-muted-foreground\">{error}</p>\n          </div>\n          <Button variant=\"outline\" size=\"sm\" onClick={onRefresh} className=\"gap-2\">\n            <RefreshCw data-icon=\"inline-start\" />\n            Retry\n          </Button>\n        </div>\n      </div>\n    )\n  }\n\n  // Empty state\n  if (listings.length === 0) {\n    return (\n      <div className={cn(\"flex flex-col gap-6\", className)}>\n        <div className=\"flex flex-col items-center justify-center gap-4 rounded-lg border border-dashed p-10\">\n          <div className=\"mx-auto flex size-12 items-center justify-center rounded-full bg-muted\">\n            <Store className=\"size-6 text-muted-foreground\" />\n          </div>\n          <div className=\"flex flex-col gap-1 text-center\">\n            <p className=\"text-sm font-medium\">No listings found</p>\n            <p className=\"text-xs text-muted-foreground\">\n              There are no ordinals listed for sale right now. Check back later.\n            </p>\n          </div>\n          <Button variant=\"outline\" size=\"sm\" onClick={onRefresh} className=\"gap-2\">\n            <RefreshCw data-icon=\"inline-start\" />\n            Refresh\n          </Button>\n        </div>\n      </div>\n    )\n  }\n\n  // Grid with listings\n  return (\n    <div className={cn(\"flex flex-col gap-6\", className)}>\n      <div className=\"grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4\">\n        {listings.map((listing) => (\n          <ListingCardUI\n            key={listing.outpoint}\n            outpoint={listing.outpoint}\n            price={listing.price}\n            seller={listing.seller}\n            contentType={listing.contentType}\n            thumbnailUrl={`${ORDFS_CONTENT_URL}/${listing.origin}`}\n            name={listing.name}\n            onBuy={onBuy}\n            onListingClick={onListingClick}\n            onExternalLink={onExternalLink}\n          />\n        ))}\n      </div>\n\n      {/* Inline error after partial load */}\n      {error && listings.length > 0 && (\n        <div className=\"flex items-center justify-center gap-2 text-sm text-destructive\">\n          <AlertCircle className=\"size-4\" />\n          <span>{error}</span>\n        </div>\n      )}\n\n      {/* Load more */}\n      {hasMore && (\n        <div className=\"flex justify-center\">\n          <Button\n            variant=\"outline\"\n            onClick={onLoadMore}\n            disabled={isLoadingMore}\n            className=\"gap-2\"\n          >\n            {isLoadingMore ? (\n              <>\n                <Loader2 className=\"animate-spin\" data-icon=\"inline-start\" />\n                Loading...\n              </>\n            ) : (\n              \"Load More\"\n            )}\n          </Button>\n        </div>\n      )}\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "~/components/blocks/market-grid/market-grid-ui.tsx"
    },
    {
      "path": "registry/new-york/blocks/market-grid/use-market-grid.ts",
      "content": "import { useCallback, useEffect, useRef, useState } from \"react\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Raw TXO record from the 1sat-stack API */\nexport interface OneSatTxo {\n  /** Transaction ID */\n  txid: string\n  /** Output index */\n  vout: number\n  /** Outpoint in \"txid.vout\" format */\n  outpoint: string\n  /** Satoshi value of the output */\n  satoshis: number\n  /** Locking script hex */\n  script: string\n  /** Content type from inscription */\n  type?: string\n  /** Origin outpoint for the inscription */\n  origin?: string\n  /** MAP metadata */\n  MAP?: Record<string, string>\n  /** Listing price from ordlock decode (sats) */\n  price?: number\n  /** Seller address decoded from ordlock */\n  seller?: string\n  /** BMAP metadata name */\n  name?: string\n}\n\n/** Normalized listing for display */\nexport interface MarketListing {\n  /** Outpoint of the listing (txid.vout) */\n  outpoint: string\n  /** Listing price in satoshis */\n  price: number\n  /** Seller address (base58check) */\n  seller: string\n  /** Content type (MIME type) */\n  contentType: string\n  /** Origin outpoint for ORDFS thumbnail */\n  origin: string\n  /** Optional name from MAP metadata */\n  name: string | null\n}\n\nexport type SortField = \"price\" | \"recent\"\nexport type SortDirection = \"asc\" | \"desc\"\n\nexport interface UseMarketGridOptions {\n  /** Base URL of the 1sat-stack API */\n  apiUrl?: string\n  /** Number of listings per page */\n  pageSize?: number\n  /** Sort field */\n  sortBy?: SortField\n  /** Sort direction */\n  sortDir?: SortDirection\n  /** Optional content type filter (e.g. \"image/png\") */\n  contentTypeFilter?: string\n}\n\nexport interface UseMarketGridReturn {\n  /** Currently loaded listings */\n  listings: MarketListing[]\n  /** Whether the initial load is in progress */\n  isLoading: boolean\n  /** Whether a subsequent page is loading */\n  isLoadingMore: boolean\n  /** Error from the most recent fetch */\n  error: string | null\n  /** Whether there are more pages to load */\n  hasMore: boolean\n  /** Load the next page */\n  loadMore: () => void\n  /** Re-fetch from the beginning */\n  refresh: () => void\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_API_URL = \"https://api.1sat.app\"\nconst DEFAULT_PAGE_SIZE = 20\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction normalizeTxo(txo: OneSatTxo): MarketListing | null {\n  const price = txo.price ?? 0\n  if (price <= 0) return null\n\n  return {\n    outpoint: txo.outpoint ?? `${txo.txid}.${txo.vout}`,\n    price,\n    seller: txo.seller ?? \"\",\n    contentType: txo.type ?? \"application/octet-stream\",\n    origin: txo.origin ?? txo.outpoint ?? `${txo.txid}.${txo.vout}`,\n    name: txo.name ?? txo.MAP?.name ?? null,\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\nexport function useMarketGrid({\n  apiUrl = DEFAULT_API_URL,\n  pageSize = DEFAULT_PAGE_SIZE,\n  sortBy = \"recent\",\n  sortDir = \"desc\",\n  contentTypeFilter,\n}: UseMarketGridOptions = {}): UseMarketGridReturn {\n  const [listings, setListings] = useState<MarketListing[]>([])\n  const [isLoading, setIsLoading] = useState(true)\n  const [isLoadingMore, setIsLoadingMore] = useState(false)\n  const [error, setError] = useState<string | null>(null)\n  const [hasMore, setHasMore] = useState(true)\n  const offsetRef = useRef(0)\n  const abortRef = useRef<AbortController | null>(null)\n\n  const fetchPage = useCallback(\n    async (offset: number, signal: AbortSignal): Promise<MarketListing[]> => {\n      const params = new URLSearchParams({\n        tags: \"ordlock\",\n        unspent: \"true\",\n        limit: String(pageSize),\n        offset: String(offset),\n      })\n\n      if (contentTypeFilter) {\n        params.set(\"type\", contentTypeFilter)\n      }\n\n      if (sortBy === \"price\") {\n        params.set(\"sort\", \"price\")\n        params.set(\"dir\", sortDir)\n      }\n\n      const url = `${apiUrl}/1sat/txo/search?${params.toString()}`\n\n      const response = await fetch(url, { signal })\n\n      if (!response.ok) {\n        throw new Error(`API returned ${response.status}: ${response.statusText}`)\n      }\n\n      const data: OneSatTxo[] = await response.json()\n\n      const normalized: MarketListing[] = []\n      for (const txo of data) {\n        const listing = normalizeTxo(txo)\n        if (listing) normalized.push(listing)\n      }\n\n      return normalized\n    },\n    [apiUrl, pageSize, sortBy, sortDir, contentTypeFilter],\n  )\n\n  const loadInitial = useCallback(async () => {\n    abortRef.current?.abort()\n    const controller = new AbortController()\n    abortRef.current = controller\n\n    setIsLoading(true)\n    setError(null)\n    setListings([])\n    offsetRef.current = 0\n\n    try {\n      const page = await fetchPage(0, controller.signal)\n      setListings(page)\n      offsetRef.current = page.length\n      setHasMore(page.length >= pageSize)\n    } catch (err) {\n      if (err instanceof DOMException && err.name === \"AbortError\") return\n      const msg = err instanceof Error ? err.message : \"Failed to load listings\"\n      setError(msg)\n    } finally {\n      setIsLoading(false)\n    }\n  }, [fetchPage, pageSize])\n\n  const loadMore = useCallback(async () => {\n    if (isLoadingMore || !hasMore) return\n\n    abortRef.current?.abort()\n    const controller = new AbortController()\n    abortRef.current = controller\n\n    setIsLoadingMore(true)\n    setError(null)\n\n    try {\n      const page = await fetchPage(offsetRef.current, controller.signal)\n      setListings((prev) => [...prev, ...page])\n      offsetRef.current += page.length\n      setHasMore(page.length >= pageSize)\n    } catch (err) {\n      if (err instanceof DOMException && err.name === \"AbortError\") return\n      const msg = err instanceof Error ? err.message : \"Failed to load more listings\"\n      setError(msg)\n    } finally {\n      setIsLoadingMore(false)\n    }\n  }, [fetchPage, pageSize, isLoadingMore, hasMore])\n\n  // Load on mount and when fetch params change\n  useEffect(() => {\n    loadInitial()\n\n    return () => {\n      abortRef.current?.abort()\n    }\n  }, [loadInitial])\n\n  return {\n    listings,\n    isLoading,\n    isLoadingMore,\n    error,\n    hasMore,\n    loadMore,\n    refresh: loadInitial,\n  }\n}\n",
      "type": "registry:component",
      "target": "~/components/blocks/market-grid/use-market-grid.ts"
    }
  ],
  "type": "registry:block"
}