{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "cart-provider", "title": "Cart Provider", "description": "Optional, backend-agnostic cart state. A CartProvider context and useCart hook that add lines, merge duplicate variants, update quantities, and compute the subtotal. Sync to your backend through onLinesChange.", "registryDependencies": [ "https://lumo-www.vercel.app/r/lib.json" ], "files": [ { "path": "packages/core/src/components/lumo/cart-provider.tsx", "content": "\"use client\"\r\n\r\nimport * as React from \"react\"\r\nimport type { CartLine, Money, Product, Variant } from \"@/lib/lumo/types\"\r\nimport { addMoney, multiplyMoney, zeroMoney } from \"@/lib/lumo/money\"\r\n\r\n/** The product fields a cart line needs to display itself. */\r\nexport type CartProductRef = Pick\r\n\r\nexport interface CartContextValue {\r\n lines: CartLine[]\r\n subtotal: Money\r\n totalQuantity: number\r\n currency: string\r\n isOpen: boolean\r\n addLine: (product: CartProductRef, variant: Variant, quantity?: number) => void\r\n removeLine: (lineId: string) => void\r\n updateQuantity: (lineId: string, quantity: number) => void\r\n clear: () => void\r\n setOpen: (open: boolean) => void\r\n openCart: () => void\r\n closeCart: () => void\r\n}\r\n\r\nconst CartContext = React.createContext(null)\r\n\r\nfunction lineTotalFor(variant: Variant, quantity: number): Money {\r\n return multiplyMoney(variant.price, quantity)\r\n}\r\n\r\nfunction computeSubtotal(lines: CartLine[], currency: string): Money {\r\n if (lines.length === 0) {\r\n return zeroMoney(currency)\r\n }\r\n return addMoney(...lines.map((line) => line.lineTotal))\r\n}\r\n\r\nexport interface CartProviderProps {\r\n children: React.ReactNode\r\n /** Currency used while the cart is empty. Defaults to 'USD'. */\r\n currency?: string\r\n /** Initial lines, for hydrating from storage or a backend. */\r\n initialLines?: CartLine[]\r\n /** Fired whenever the lines change, for syncing to your backend. */\r\n onLinesChange?: (lines: CartLine[]) => void\r\n}\r\n\r\n/**\r\n * Optional, backend-agnostic cart state. Lumo components are presentational by\r\n * default; opt into this provider when you want client-side cart state and sync\r\n * changes to your backend through `onLinesChange`.\r\n */\r\nexport function CartProvider({\r\n children,\r\n currency = \"USD\",\r\n initialLines = [],\r\n onLinesChange,\r\n}: CartProviderProps) {\r\n const [lines, setLines] = React.useState(initialLines)\r\n const [isOpen, setIsOpen] = React.useState(false)\r\n\r\n const isFirstRender = React.useRef(true)\r\n React.useEffect(() => {\r\n if (isFirstRender.current) {\r\n isFirstRender.current = false\r\n return\r\n }\r\n onLinesChange?.(lines)\r\n }, [lines, onLinesChange])\r\n\r\n const addLine = React.useCallback(\r\n (product: CartProductRef, variant: Variant, quantity = 1) => {\r\n setLines((prev) => {\r\n const existing = prev.find((line) => line.variant.id === variant.id)\r\n if (existing) {\r\n return prev.map((line) =>\r\n line.variant.id === variant.id\r\n ? {\r\n ...line,\r\n quantity: line.quantity + quantity,\r\n lineTotal: lineTotalFor(variant, line.quantity + quantity),\r\n }\r\n : line\r\n )\r\n }\r\n return [\r\n ...prev,\r\n {\r\n id: variant.id,\r\n variant,\r\n product,\r\n quantity,\r\n lineTotal: lineTotalFor(variant, quantity),\r\n },\r\n ]\r\n })\r\n },\r\n []\r\n )\r\n\r\n const removeLine = React.useCallback((lineId: string) => {\r\n setLines((prev) => prev.filter((line) => line.id !== lineId))\r\n }, [])\r\n\r\n const updateQuantity = React.useCallback((lineId: string, quantity: number) => {\r\n setLines((prev) => {\r\n if (quantity <= 0) {\r\n return prev.filter((line) => line.id !== lineId)\r\n }\r\n return prev.map((line) =>\r\n line.id === lineId\r\n ? { ...line, quantity, lineTotal: lineTotalFor(line.variant, quantity) }\r\n : line\r\n )\r\n })\r\n }, [])\r\n\r\n const clear = React.useCallback(() => setLines([]), [])\r\n const openCart = React.useCallback(() => setIsOpen(true), [])\r\n const closeCart = React.useCallback(() => setIsOpen(false), [])\r\n\r\n const value = React.useMemo(() => {\r\n const activeCurrency = lines[0]?.variant.price.currency ?? currency\r\n return {\r\n lines,\r\n subtotal: computeSubtotal(lines, activeCurrency),\r\n totalQuantity: lines.reduce((total, line) => total + line.quantity, 0),\r\n currency: activeCurrency,\r\n isOpen,\r\n addLine,\r\n removeLine,\r\n updateQuantity,\r\n clear,\r\n setOpen: setIsOpen,\r\n openCart,\r\n closeCart,\r\n }\r\n }, [lines, currency, isOpen, addLine, removeLine, updateQuantity, clear, openCart, closeCart])\r\n\r\n return {children}\r\n}\r\n\r\n/** Read the cart state. Must be used within a `CartProvider`. */\r\nexport function useCart(): CartContextValue {\r\n const context = React.useContext(CartContext)\r\n if (!context) {\r\n throw new Error(\"useCart must be used within a \")\r\n }\r\n return context\r\n}\r\n", "type": "registry:hook", "target": "components/lumo/cart-provider.tsx" } ], "type": "registry:hook" }