{
  "name": "variant-selector",
  "title": "VariantSelector",
  "description": "Select product variants via axis chips or direct list.",
  "type": "component",
  "registryDependencies": [
    "price"
  ],
  "files": [
    {
      "path": "variant-selector.tsx",
      "content": "\"use client\";\n\nimport React, { useState, useEffect, useRef, useId } from \"react\";\nimport { RadioGroup } from \"@base-ui/react/radio-group\";\nimport { Radio } from \"@base-ui/react/radio\";\nimport type { VariantView, VariantAxisWithValues } from \"@cimplify/sdk\";\nimport type { Money, CurrencyCode } from \"@cimplify/sdk\";\nimport { parsePrice } from \"@cimplify/sdk\";\nimport { getVariantDisplayName } from \"@cimplify/sdk\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface VariantSelectorClassNames {\n  root?: string;\n  axisLabel?: string;\n  axisOptions?: string;\n  option?: string;\n  optionSelected?: string;\n  swatch?: string;\n  swatchSelected?: string;\n  listLabel?: string;\n  list?: string;\n  name?: string;\n  pricing?: string;\n  adjustment?: string;\n}\n\nexport interface VariantSelectorProps {\n  variants: VariantView[];\n  variantAxes?: VariantAxisWithValues[];\n  basePrice?: Money;\n  currency?: CurrencyCode;\n  selectedVariantId?: string;\n  onVariantChange: (variantId: string | undefined, variant: VariantView | undefined) => void;\n  productName?: string;\n  className?: string;\n  classNames?: VariantSelectorClassNames;\n}\n\nexport function VariantSelector({\n  variants,\n  variantAxes,\n  basePrice,\n  currency,\n  selectedVariantId,\n  onVariantChange,\n  productName,\n  className,\n  classNames,\n}: VariantSelectorProps): React.ReactElement | null {\n  const [axisSelections, setAxisSelections] = useState<Record<string, string>>({});\n  const initialized = useRef(false);\n  const idPrefix = useId();\n\n  useEffect(() => {\n    initialized.current = false;\n  }, [variants]);\n\n  useEffect(() => {\n    if (initialized.current) return;\n    if (!variants || variants.length === 0) return;\n\n    const defaultVariant = variants.find((v) => v.is_default) || variants[0];\n    if (!defaultVariant) return;\n\n    initialized.current = true;\n    onVariantChange(defaultVariant.id, defaultVariant);\n\n    if (defaultVariant.display_attributes) {\n      const initial: Record<string, string> = {};\n      for (const attr of defaultVariant.display_attributes) {\n        initial[attr.axis_id] = attr.value_id;\n      }\n      setAxisSelections(initial);\n    }\n  }, [variants, onVariantChange]);\n\n  useEffect(() => {\n    if (!initialized.current) return;\n    if (!variantAxes || variantAxes.length === 0) return;\n\n    const match = variants.find((v) => {\n      if (!v.display_attributes) return false;\n      return v.display_attributes.every(\n        (attr) => axisSelections[attr.axis_id] === attr.value_id,\n      );\n    });\n\n    if (match && match.id !== selectedVariantId) {\n      onVariantChange(match.id, match);\n    }\n  }, [axisSelections, variants, variantAxes, selectedVariantId, onVariantChange]);\n\n  if (!variants || variants.length <= 1) {\n    return null;\n  }\n\n  const basePriceNum = basePrice != null ? parsePrice(basePrice) : 0;\n\n  if (variantAxes && variantAxes.length > 0) {\n    return (\n      <div data-cimplify-variant-selector className={cn(\"space-y-5\", className, classNames?.root)}>\n        {variantAxes.map((axis) => {\n          const labelId = `${idPrefix}-axis-${axis.id}`;\n          return (\n            <div key={axis.id} data-cimplify-variant-axis>\n              <label\n                id={labelId}\n                data-cimplify-variant-axis-label\n                className={cn(\"block text-xs font-medium uppercase tracking-wider text-muted-foreground mb-3\", classNames?.axisLabel)}\n              >\n                {axis.name}\n              </label>\n              <RadioGroup\n                aria-labelledby={labelId}\n                value={axisSelections[axis.id] ?? \"\"}\n                onValueChange={(value) => {\n                  setAxisSelections((prev) => ({\n                    ...prev,\n                    [axis.id]: value,\n                  }));\n                }}\n                data-cimplify-variant-axis-options\n                className={cn(\"flex flex-wrap gap-2\", classNames?.axisOptions)}\n              >\n                {axis.values.map((value) => {\n                  const isSelected = axisSelections[axis.id] === value.id;\n\n                  // Color/image axis values render as swatches; selection is a ring\n                  // so the swatch keeps its own fill or photo.\n                  if (value.image_url || value.color_hex) {\n                    const isImage = Boolean(value.image_url);\n                    return (\n                      <Radio.Root\n                        key={value.id}\n                        value={value.id}\n                        data-cimplify-variant-option\n                        data-cimplify-variant-swatch\n                        data-selected={isSelected || undefined}\n                        aria-label={value.name}\n                        title={value.name}\n                        style={isImage ? undefined : { backgroundColor: value.color_hex }}\n                        className={cn(\n                          \"relative overflow-hidden border border-border transition-shadow\",\n                          isImage ? \"h-12 w-12 rounded-md\" : \"h-9 w-9 rounded-full\",\n                          isSelected && \"ring-2 ring-foreground ring-offset-2 ring-offset-background border-transparent\",\n                          isSelected ? classNames?.swatchSelected : classNames?.swatch,\n                        )}\n                      >\n                        {isImage && (\n                          <img\n                            src={value.image_url}\n                            alt=\"\"\n                            className=\"h-full w-full object-cover\"\n                          />\n                        )}\n                      </Radio.Root>\n                    );\n                  }\n\n                  return (\n                    <Radio.Root\n                      key={value.id}\n                      value={value.id}\n                                            data-cimplify-variant-option\n                      data-selected={isSelected || undefined}\n                      className={cn(\n                        \"px-4 py-2 border text-sm font-medium transition-colors border-border hover:border-primary/50\",\n                        isSelected && \"bg-primary text-primary-foreground border-primary\",\n                        isSelected ? classNames?.optionSelected : classNames?.option,\n                      )}\n                    >\n                      {value.name}\n                    </Radio.Root>\n                  );\n                })}\n              </RadioGroup>\n            </div>\n          );\n        })}\n      </div>\n    );\n  }\n\n  const listLabelId = `${idPrefix}-variant-list`;\n\n  return (\n    <div data-cimplify-variant-selector className={cn(className, classNames?.root)}>\n      <div\n        data-cimplify-variant-list-header\n        className={cn(\"flex items-center justify-between py-3\", classNames?.listLabel)}\n      >\n        <label id={listLabelId} className=\"text-base font-bold\">\n          Options\n        </label>\n        <span className=\"text-xs font-semibold text-destructive bg-destructive/10 px-2.5 py-1 rounded\">\n          Required\n        </span>\n      </div>\n      <RadioGroup\n        aria-labelledby={listLabelId}\n        value={selectedVariantId ?? \"\"}\n        onValueChange={(value) => {\n          const variant = variants.find((v) => v.id === value);\n          onVariantChange(variant?.id, variant);\n        }}\n        data-cimplify-variant-list\n        className={cn(\"divide-y divide-border\", classNames?.list)}\n      >\n        {variants.map((variant) => {\n          const isSelected = selectedVariantId === variant.id;\n          const adjustment = parsePrice(variant.price_adjustment);\n\n          return (\n            <Radio.Root\n              key={variant.id}\n              value={variant.id}\n              data-cimplify-variant-option\n              data-selected={isSelected || undefined}\n              className={cn(\n                \"w-full flex items-center gap-3 py-4 transition-colors cursor-pointer\",\n                isSelected ? classNames?.optionSelected : classNames?.option,\n              )}\n            >\n              <span\n                data-cimplify-variant-radio\n                className={cn(\n                  \"w-5 h-5 rounded-full border-2 flex items-center justify-center shrink-0 transition-colors\",\n                  isSelected ? \"border-primary\" : \"border-muted-foreground/30\",\n                )}\n              >\n                {isSelected && <span className=\"w-2.5 h-2.5 rounded-full bg-primary\" />}\n              </span>\n              <span\n                data-cimplify-variant-name\n                className={cn(\"flex-1 min-w-0 text-sm\", classNames?.name)}\n              >\n                {getVariantDisplayName(variant, productName)}\n              </span>\n              <span data-cimplify-variant-pricing className={cn(\"text-sm text-muted-foreground\", classNames?.pricing)}>\n                {adjustment > 0 ? \"+\" : adjustment < 0 ? \"\" : \"+\"}\n                <Price amount={variant.price_adjustment} currency={currency} />\n              </span>\n            </Radio.Root>\n          );\n        })}\n      </RadioGroup>\n    </div>\n  );\n}\n"
    }
  ]
}
