{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-variant-selection",
  "title": "useVariantSelection",
  "description": "Manages variant selection and server-side re-resolution via an injected resolver, keeping the API key on your server.",
  "dependencies": [
    "@channel3/sdk",
    "@tanstack/react-query"
  ],
  "registryDependencies": [
    "https://ui.trychannel3.com/r/variants.json"
  ],
  "files": [
    {
      "path": "registry/default/hooks/use-variant-selection.ts",
      "content": "import * as React from \"react\";\nimport { useMutation } from \"@tanstack/react-query\";\nimport type { OptionValue, Product } from \"@channel3/sdk/resources\";\n\nimport { mergeSelection, selectionFromVariants } from \"@/registry/default/lib/variants\";\n\nexport interface VariantResolveInput {\n  product: Product;\n  optionName: string;\n  value: OptionValue;\n  selection: Record<string, string>;\n}\n\n/**\n * Server-side resolver. When `value.product_id` is set, fetch that product;\n * otherwise retrieve the current id with `selected_options: selection`.\n *\n * ```ts\n * client.products.retrieve({\n *   product_id: value.product_id ?? product.id,\n *   selected_options: value.product_id ? undefined : selection,\n * })\n * ```\n */\nexport type VariantResolver = (input: VariantResolveInput) => Promise<Product>;\n\nexport interface UseVariantSelectionOptions {\n  product: Product;\n  /**\n   * Re-resolves the product when a value is selected. Omit for a read-only\n   * selector that only tracks selection locally.\n   */\n  resolve?: VariantResolver;\n  onResolved?: (product: Product) => void;\n  onError?: (error: unknown) => void;\n}\n\nexport interface UseVariantSelectionResult {\n  product: Product;\n  selection: Record<string, string>;\n  isResolving: boolean;\n  error: unknown;\n  select: (optionName: string, value: OptionValue) => void;\n  reset: () => void;\n}\n\nconst EMPTY_SELECTION: Record<string, string> = {};\n\n/**\n * Selection is optimistic while `resolve` runs; the resolved product's\n * `variants.selected` is then the source of truth (including server-side\n * relaxation).\n */\nexport function useVariantSelection({\n  product: initialProduct,\n  resolve,\n  onResolved,\n  onError,\n}: UseVariantSelectionOptions): UseVariantSelectionResult {\n  const [product, setProduct] = React.useState(initialProduct);\n  const [pending, setPending] = React.useState<Record<string, string>>(EMPTY_SELECTION);\n  const [isResolving, setIsResolving] = React.useState(false);\n  const [error, setError] = React.useState<unknown>(null);\n  const generation = React.useRef(0);\n\n  const { mutate, reset: resetMutation } = useMutation({\n    mutationFn: (input: VariantResolveInput) => Promise.resolve(resolve!(input)),\n  });\n\n  const lastInputId = React.useRef(initialProduct.id);\n  React.useEffect(() => {\n    if (initialProduct.id !== lastInputId.current) {\n      lastInputId.current = initialProduct.id;\n      generation.current += 1;\n      resetMutation();\n      setProduct(initialProduct);\n      setPending(EMPTY_SELECTION);\n      setError(null);\n      setIsResolving(false);\n    }\n  }, [initialProduct, resetMutation]);\n\n  const selection = React.useMemo(() => {\n    const base = product.variants ? selectionFromVariants(product.variants) : EMPTY_SELECTION;\n    return { ...base, ...pending };\n  }, [product, pending]);\n\n  const select = React.useCallback(\n    (optionName: string, value: OptionValue) => {\n      const nextSelection = product.variants\n        ? mergeSelection(product.variants, { ...pending, [optionName]: value.label })\n        : { [optionName]: value.label };\n\n      setPending((prev) => ({ ...prev, [optionName]: value.label }));\n      setError(null);\n\n      if (!resolve) {\n        return;\n      }\n\n      const ticket = ++generation.current;\n      setIsResolving(true);\n      mutate(\n        { product, optionName, value, selection: nextSelection },\n        {\n          onSuccess: (resolved) => {\n            if (generation.current !== ticket) {\n              return;\n            }\n            setProduct(resolved);\n            setPending(EMPTY_SELECTION);\n            onResolved?.(resolved);\n          },\n          onError: (caught) => {\n            if (generation.current !== ticket) {\n              return;\n            }\n            setError(caught);\n            onError?.(caught);\n          },\n          onSettled: () => {\n            if (generation.current !== ticket) {\n              return;\n            }\n            setIsResolving(false);\n          },\n        },\n      );\n    },\n    [product, pending, resolve, onResolved, onError, mutate],\n  );\n\n  const reset = React.useCallback(() => {\n    generation.current += 1;\n    resetMutation();\n    setPending(EMPTY_SELECTION);\n    setError(null);\n    setIsResolving(false);\n  }, [resetMutation]);\n\n  return {\n    product,\n    selection,\n    isResolving,\n    error,\n    select,\n    reset,\n  };\n}\n",
      "type": "registry:hook"
    }
  ],
  "type": "registry:hook"
}
