{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "product-details",
  "title": "Product Details (PDP)",
  "description": "Compound product detail page: gallery, header (all brands), variant selection, offers, price history, description, attributes, and an optional 'you might also like' carousel. Pairs with useVariantSelection.",
  "dependencies": [
    "@channel3/sdk"
  ],
  "registryDependencies": [
    "separator",
    "https://ui.trychannel3.com/r/image-gallery.json",
    "https://ui.trychannel3.com/r/offers-list.json",
    "https://ui.trychannel3.com/r/variant-selector.json",
    "https://ui.trychannel3.com/r/price-range-gauge.json",
    "https://ui.trychannel3.com/r/price-history-chart.json",
    "https://ui.trychannel3.com/r/product-attributes.json",
    "https://ui.trychannel3.com/r/product-recommendations.json",
    "https://ui.trychannel3.com/r/use-product-recommendations.json",
    "https://ui.trychannel3.com/r/use-variant-selection.json",
    "https://ui.trychannel3.com/r/format.json"
  ],
  "files": [
    {
      "path": "registry/default/components/product-details.tsx",
      "content": "import * as React from \"react\";\nimport type { OptionValue, PriceHistoryResponse, Product, ProductOffer } from \"@channel3/sdk/resources\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { ImageGallery } from \"@/registry/default/components/image-gallery\";\nimport { OffersList } from \"@/registry/default/components/offers-list\";\nimport { PriceHistoryChart } from \"@/registry/default/components/price-history-chart\";\nimport { PriceRangeGauge } from \"@/registry/default/components/price-range-gauge\";\nimport { ProductAttributes } from \"@/registry/default/components/product-attributes\";\nimport { ProductRecommendations } from \"@/registry/default/components/product-recommendations\";\nimport { VariantSelector } from \"@/registry/default/components/variant-selector\";\nimport type { SimilarFetcher } from \"@/registry/default/hooks/use-product-recommendations\";\nimport { formatCurrency, formatPrice, isInStock, isOnSale, leadOffer } from \"@/registry/default/lib/format\";\n\nexport interface ProductDetailsRecommendationsConfig {\n  limit?: number;\n  title?: React.ReactNode;\n  eager?: boolean;\n  skeletonCount?: number;\n  getHref?: (product: Product) => string;\n  onSelect?: (product: Product) => void;\n  onPreload?: (product: Product) => void;\n  onSelectVariant?: (product: Product, value: OptionValue) => void;\n  showSwatches?: boolean;\n}\n\ninterface ProductDetailsContextValue {\n  product: Product;\n  selection: Record<string, string> | undefined;\n  onSelectVariant: ((optionName: string, value: OptionValue) => void) | undefined;\n  onOfferClick: ((offer: ProductOffer) => void) | undefined;\n  buyLinkRel: string | undefined;\n  priceHistory: PriceHistoryResponse | undefined;\n  isResolving: boolean;\n  locale: string | undefined;\n  /** A hovered swatch's value, previewed in the gallery (no fetch). */\n  variantPreview: OptionValue | null;\n  setVariantPreview: (value: OptionValue | null) => void;\n  fetchSimilar: SimilarFetcher | undefined;\n  recommendations: ProductDetailsRecommendationsConfig | undefined;\n}\n\nconst ProductDetailsContext = React.createContext<ProductDetailsContextValue | null>(null);\n\nfunction useProductDetails(component: string): ProductDetailsContextValue {\n  const context = React.useContext(ProductDetailsContext);\n  if (!context) {\n    throw new Error(`${component} must be used within <ProductDetails> or <ProductDetailsRoot>`);\n  }\n  return context;\n}\n\nexport interface ProductDetailsProps extends Omit<React.ComponentProps<\"div\">, \"onSelect\"> {\n  product: Product;\n  /** Controlled variant selection (`{ optionName: label }`); defaults to `variants.selected`. */\n  selection?: Record<string, string>;\n  /** Fired when a variant value is chosen — wire to {@link useVariantSelection}. */\n  onSelectVariant?: (optionName: string, value: OptionValue) => void;\n  onOfferClick?: (offer: ProductOffer) => void;\n  /** `rel` for merchant buy links. Use `\"sponsored noopener noreferrer\"` for affiliate links. */\n  buyLinkRel?: string;\n  priceHistory?: PriceHistoryResponse;\n  isResolving?: boolean;\n  locale?: string;\n  /**\n   * Server-side fetcher wrapping `client.products.findSimilar`. When provided,\n   * the default layout renders a lazy \"you might also like\" carousel below the\n   * grid, and `ProductDetailsRecommendations` becomes available.\n   */\n  fetchSimilar?: SimilarFetcher;\n  recommendations?: ProductDetailsRecommendationsConfig;\n}\n\nfunction Root({\n  product,\n  selection,\n  onSelectVariant,\n  onOfferClick,\n  buyLinkRel,\n  priceHistory,\n  isResolving = false,\n  locale,\n  fetchSimilar,\n  recommendations,\n  children,\n  ...rest\n}: ProductDetailsProps) {\n  const [variantPreview, setVariantPreview] = React.useState<OptionValue | null>(null);\n\n  const value = React.useMemo<ProductDetailsContextValue>(\n    () => ({\n      product,\n      selection,\n      onSelectVariant,\n      onOfferClick,\n      buyLinkRel,\n      priceHistory,\n      isResolving,\n      locale,\n      variantPreview,\n      setVariantPreview,\n      fetchSimilar,\n      recommendations,\n    }),\n    [\n      product,\n      selection,\n      onSelectVariant,\n      onOfferClick,\n      buyLinkRel,\n      priceHistory,\n      isResolving,\n      locale,\n      variantPreview,\n      fetchSimilar,\n      recommendations,\n    ],\n  );\n\n  return (\n    <ProductDetailsContext.Provider value={value}>\n      <div data-slot=\"product-details\" {...rest}>\n        {children}\n      </div>\n    </ProductDetailsContext.Provider>\n  );\n}\n\nfunction Gallery({ className, ...rest }: React.ComponentProps<\"div\">) {\n  const { product, variantPreview } = useProductDetails(\"ProductDetailsGallery\");\n  return (\n    <ImageGallery\n      images={product.images ?? []}\n      previewSrc={variantPreview?.thumbnail_url ?? null}\n      className={className}\n      {...rest}\n    />\n  );\n}\n\nfunction Header({ className, ...rest }: React.ComponentProps<\"div\">) {\n  const { product, locale } = useProductDetails(\"ProductDetailsHeader\");\n  const brands = (product.brands ?? []).map((brand) => brand.name).filter(Boolean);\n  const offer = leadOffer(product.offers);\n\n  return (\n    <div className={cn(\"flex flex-col gap-2\", className)} {...rest}>\n      {brands.length > 0 ? (\n        <span className=\"text-sm text-muted-foreground\">{brands.join(\" · \")}</span>\n      ) : null}\n      <h1 className=\"text-xl leading-tight font-semibold\">{product.title}</h1>\n      {offer ? (\n        <div className=\"flex items-center gap-3 pt-1\">\n          <span className=\"text-2xl font-semibold\">{formatPrice(offer.price, locale)}</span>\n          {isOnSale(offer.price) && offer.price.compare_at_price ? (\n            <span className=\"text-base text-muted-foreground line-through\">\n              {formatCurrency(offer.price.compare_at_price, offer.price.currency, locale)}\n            </span>\n          ) : null}\n        </div>\n      ) : null}\n    </div>\n  );\n}\n\nfunction Variants({ className, ...rest }: React.ComponentProps<\"div\">) {\n  const { product, selection, onSelectVariant, isResolving, setVariantPreview } = useProductDetails(\n    \"ProductDetailsVariants\",\n  );\n  if (!product.variants || product.variants.options.length === 0) {\n    return null;\n  }\n  return (\n    <div\n      aria-busy={isResolving}\n      className={cn(isResolving && \"pointer-events-none opacity-60\", className)}\n      {...rest}\n    >\n      <VariantSelector\n        variants={product.variants}\n        value={selection}\n        onSelect={onSelectVariant}\n        onValuePreview={setVariantPreview}\n      />\n    </div>\n  );\n}\n\nfunction Offers({ className, ...rest }: React.ComponentProps<\"div\">) {\n  const { product, onOfferClick, locale, buyLinkRel } = useProductDetails(\"ProductDetailsOffers\");\n  const offers = product.offers ?? [];\n  if (offers.length === 0) {\n    return null;\n  }\n  // Drop the heading when every offer is out of stock — OffersList already says so.\n  const hasInStock = offers.some((offer) => isInStock(offer.availability));\n  return (\n    <div className={cn(\"flex flex-col gap-2\", className)} {...rest}>\n      {hasInStock ? (\n        <h2 className=\"text-sm font-medium text-muted-foreground\">Available at</h2>\n      ) : null}\n      <OffersList\n        offers={offers}\n        onOfferClick={onOfferClick}\n        locale={locale}\n        buyLinkRel={buyLinkRel}\n      />\n    </div>\n  );\n}\n\nfunction PriceHistorySection({ className, ...rest }: React.ComponentProps<\"div\">) {\n  const { priceHistory, locale } = useProductDetails(\"ProductDetailsPriceHistory\");\n  const statistics = priceHistory?.statistics ?? undefined;\n  const history = priceHistory?.history ?? [];\n  if (!statistics && history.length === 0) {\n    return null;\n  }\n  return (\n    <div className={cn(\"flex flex-col gap-4\", className)} {...rest}>\n      <h2 className=\"text-sm font-medium text-muted-foreground\">Price history</h2>\n      {statistics ? <PriceRangeGauge statistics={statistics} locale={locale} /> : null}\n      {history.length > 0 ? <PriceHistoryChart history={history} locale={locale} /> : null}\n      <p className=\"text-xs text-muted-foreground\">Based on the last 30 days.</p>\n    </div>\n  );\n}\n\nfunction Description({ className, ...rest }: React.ComponentProps<\"div\">) {\n  const { product } = useProductDetails(\"ProductDetailsDescription\");\n  const features = product.key_features ?? [];\n  if (!product.description && features.length === 0) {\n    return null;\n  }\n  return (\n    <div className={cn(\"flex flex-col gap-3\", className)} {...rest}>\n      {product.description ? (\n        <p className=\"text-sm leading-relaxed text-muted-foreground\">{product.description}</p>\n      ) : null}\n      {features.length > 0 ? (\n        <ul className=\"flex flex-col gap-1 text-sm text-muted-foreground\">\n          {features.map((feature, index) => (\n            <li key={`${feature}-${index}`} className=\"flex gap-2\">\n              <span aria-hidden>•</span>\n              <span>{feature}</span>\n            </li>\n          ))}\n        </ul>\n      ) : null}\n    </div>\n  );\n}\n\nfunction Attributes({ className, ...rest }: React.ComponentProps<\"div\">) {\n  const { product } = useProductDetails(\"ProductDetailsAttributes\");\n  if (!hasAttributes(product)) {\n    return null;\n  }\n  return (\n    <div className={cn(\"flex flex-col gap-2\", className)} {...rest}>\n      <h2 className=\"text-sm font-medium text-muted-foreground\">Details</h2>\n      <ProductAttributes product={product} />\n    </div>\n  );\n}\n\nexport interface ProductDetailsRecommendationsProps\n  extends Omit<React.ComponentProps<typeof ProductRecommendations>, \"productId\" | \"fetchSimilar\"> {\n  fetchSimilar?: SimilarFetcher;\n}\n\nfunction Recommendations({ fetchSimilar, ...rest }: ProductDetailsRecommendationsProps) {\n  const { product, fetchSimilar: contextFetcher, recommendations } = useProductDetails(\n    \"ProductDetailsRecommendations\",\n  );\n  const fetcher = fetchSimilar ?? contextFetcher;\n  if (!fetcher) {\n    return null;\n  }\n  return (\n    <ProductRecommendations\n      productId={product.id}\n      fetchSimilar={fetcher}\n      {...recommendations}\n      {...rest}\n    />\n  );\n}\n\nfunction hasAttributes(product: Product): boolean {\n  return (\n    Boolean(product.category) ||\n    Object.keys(product.structured_attributes ?? {}).length > 0 ||\n    (product.materials?.length ?? 0) > 0 ||\n    Boolean(product.gender) ||\n    Boolean(product.age)\n  );\n}\n\nfunction DefaultLayout() {\n  const { priceHistory, fetchSimilar } = useProductDetails(\"ProductDetails\");\n\n  const showPriceHistory =\n    Boolean(priceHistory?.statistics) || (priceHistory?.history?.length ?? 0) > 0;\n\n  return (\n    <div className=\"flex flex-col gap-12\">\n      <div className=\"grid gap-8 md:grid-cols-2 md:items-start lg:gap-12\">\n        <Gallery className=\"self-start md:sticky md:top-4\" />\n        <div className=\"flex flex-col gap-6\">\n          <Header />\n          <Variants />\n          <Offers />\n          <Description />\n          <Attributes />\n          {showPriceHistory ? (\n            <>\n              <Separator />\n              <PriceHistorySection />\n            </>\n          ) : null}\n        </div>\n      </div>\n      {fetchSimilar ? <Recommendations /> : null}\n    </div>\n  );\n}\n\nexport function ProductDetails({ className, ...props }: ProductDetailsProps) {\n  return (\n    <Root className={cn(\"w-full\", className)} {...props}>\n      <DefaultLayout />\n    </Root>\n  );\n}\n\nexport {\n  Root as ProductDetailsRoot,\n  Gallery as ProductDetailsGallery,\n  Header as ProductDetailsHeader,\n  Variants as ProductDetailsVariants,\n  Offers as ProductDetailsOffers,\n  PriceHistorySection as ProductDetailsPriceHistory,\n  Description as ProductDetailsDescription,\n  Attributes as ProductDetailsAttributes,\n  Recommendations as ProductDetailsRecommendations,\n};\n",
      "type": "registry:block"
    }
  ],
  "type": "registry:block"
}
