{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "format",
  "title": "Format helpers",
  "description": "Price, currency, discount, domain, availability, and image helpers for Channel3 product data.",
  "dependencies": [
    "@channel3/sdk"
  ],
  "files": [
    {
      "path": "registry/default/lib/format.ts",
      "content": "import type {\n  AvailabilityStatus,\n  Price,\n  ProductImage,\n  ProductOffer,\n} from \"@channel3/sdk/resources\";\n\n/**\n * Format a numeric amount as a localized currency string. Falls back to a\n * plain `CODE 12.34` string when the runtime doesn't recognize the currency.\n */\nexport function formatCurrency(amount: number, currency: string, locale?: string): string {\n  try {\n    return new Intl.NumberFormat(locale, { style: \"currency\", currency }).format(amount);\n  } catch {\n    return `${currency} ${amount.toFixed(2)}`;\n  }\n}\n\n/** Format a Channel3 `Price` using its embedded currency code. */\nexport function formatPrice(price: Price, locale?: string): string {\n  return formatCurrency(price.price, price.currency, locale);\n}\n\n/** True when the price carries a higher pre-discount `compare_at_price`. */\nexport function isOnSale(price: Price): boolean {\n  return typeof price.compare_at_price === \"number\" && price.compare_at_price > price.price;\n}\n\n/**\n * Whole-number discount percentage derived from `compare_at_price`, or `null`\n * when the item isn't discounted.\n */\nexport function discountPercent(price: Price): number | null {\n  if (!isOnSale(price) || !price.compare_at_price) {\n    return null;\n  }\n  return Math.round(((price.compare_at_price - price.price) / price.compare_at_price) * 100);\n}\n\n/** Strip protocol and a leading `www.` from a retailer domain for display. */\nexport function formatDomain(domain: string): string {\n  return domain.replace(/^https?:\\/\\//, \"\").replace(/^www\\./, \"\");\n}\n\nconst IN_STOCK: ReadonlySet<AvailabilityStatus> = new Set<AvailabilityStatus>([\n  \"InStock\",\n  \"LimitedAvailability\",\n]);\n\n/**\n * The single in-stock definition used across the kit: a status is \"in stock\"\n * when it's `InStock` or `LimitedAvailability`. Everything else (pre-order,\n * back-order, sold out, …) reads as not in stock for lead-offer selection,\n * sold-out badges, and variant emphasis.\n */\nexport function isInStock(status: AvailabilityStatus): boolean {\n  return IN_STOCK.has(status);\n}\n\nconst AVAILABILITY_LABELS: Record<AvailabilityStatus, string> = {\n  InStock: \"In stock\",\n  LimitedAvailability: \"Limited availability\",\n  PreOrder: \"Pre-order\",\n  BackOrder: \"Back-order\",\n  SoldOut: \"Sold out\",\n  OutOfStock: \"Out of stock\",\n  Discontinued: \"Discontinued\",\n  Unknown: \"Unavailable\",\n};\n\n/** Human-readable label for an availability status. */\nexport function availabilityLabel(status: AvailabilityStatus): string {\n  return AVAILABILITY_LABELS[status];\n}\n\n/**\n * Pick the best image to show for a product.\n *\n * `preferCleaned` favors `is_cleaned_image` shots (square, uniform background)\n * for grid/card contexts; otherwise the main image (or first image) wins.\n */\nexport function pickImage(\n  images: ReadonlyArray<ProductImage> | undefined,\n  { preferCleaned = false }: { preferCleaned?: boolean } = {},\n): ProductImage | undefined {\n  if (!images || images.length === 0) {\n    return undefined;\n  }\n  if (preferCleaned) {\n    const cleaned = images.find((image) => image.is_cleaned_image);\n    if (cleaned) {\n      return cleaned;\n    }\n  }\n  return images.find((image) => image.is_main_image) ?? images[0];\n}\n\n/**\n * Hover/secondary image priority: a contextual shot (product worn or in use)\n * makes a far more compelling crossfade than another clean studio angle. Shot\n * types are tried in this order, then any remaining image, with reference shots\n * (size charts, packaging, etc.) excluded entirely.\n */\nconst HOVER_SHOT_PRIORITY: ReadonlyArray<NonNullable<ProductImage[\"shot_type\"]>> = [\n  \"on_model\",\n  \"lifestyle\",\n  \"in_use\",\n  \"flat_lay\",\n  \"angle_view\",\n];\n\nconst HOVER_SHOT_EXCLUDE: ReadonlySet<NonNullable<ProductImage[\"shot_type\"]>> = new Set([\n  \"size_chart\",\n  \"packaging\",\n  \"product_information\",\n  \"merchant_information\",\n  \"scale_reference\",\n]);\n\n/**\n * Pick the best image to crossfade to on hover, preferring contextual shots\n * (see {@link HOVER_SHOT_PRIORITY}). Returns `undefined` when there's no\n * suitable second image.\n */\nexport function pickHoverImage(\n  images: ReadonlyArray<ProductImage> | undefined,\n  { excludeUrl }: { excludeUrl?: string } = {},\n): ProductImage | undefined {\n  if (!images || images.length === 0) {\n    return undefined;\n  }\n  const candidates = images.filter(\n    (image) =>\n      image.url !== excludeUrl &&\n      !(image.shot_type != null && HOVER_SHOT_EXCLUDE.has(image.shot_type)),\n  );\n  if (candidates.length === 0) {\n    return undefined;\n  }\n  for (const shot of HOVER_SHOT_PRIORITY) {\n    const match = candidates.find((image) => image.shot_type === shot);\n    if (match) {\n      return match;\n    }\n  }\n  return candidates[0];\n}\n\n/** Lowest-priced offer, preferring in-stock merchants. */\nexport function leadOffer(offers: ReadonlyArray<ProductOffer> | undefined): ProductOffer | undefined {\n  if (!offers || offers.length === 0) {\n    return undefined;\n  }\n  const byPrice = [...offers].sort((a, b) => a.price.price - b.price.price);\n  return byPrice.find((offer) => isInStock(offer.availability)) ?? byPrice[0];\n}\n\n/** True when offers exist but none are in stock. */\nexport function isSoldOut(offers: ReadonlyArray<ProductOffer> | undefined): boolean {\n  return Boolean(offers && offers.length > 0 && !offers.some((o) => isInStock(o.availability)));\n}\n",
      "type": "registry:lib"
    }
  ],
  "type": "registry:lib"
}