{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "price-selector", "type": "registry:block", "title": "Price selector", "description": "Hosted checkout billing cycle selector for the dark product panel.", "files": [ { "path": "components/lomi-ui/price-selector.tsx", "target": "components/lomi-ui/price-selector.tsx", "type": "registry:component", "content": "'use client';\n\nimport * as React from 'react';\n\nexport interface PriceOption {\n price_id: string;\n amount: number;\n billing_interval?: string | null;\n is_active?: boolean;\n is_default?: boolean;\n}\n\nexport interface PriceSelectorProps {\n prices: PriceOption[];\n selectedPriceId: string | null;\n onPriceSelect: (priceId: string) => void;\n currencyCode?: string;\n embedded?: boolean;\n /** Matches checkout.billing_cycle.choose_plan */\n label?: string;\n /** Matches checkout.billing_cycle.save */\n saveLabel?: string;\n className?: string;\n}\n\nconst INTERVAL_LABELS: Record = {\n day: 'day',\n week: 'week',\n 'bi-weekly': 'bi-weekly',\n month: 'month',\n monthly: 'month',\n 'bi-monthly': 'bi-monthly',\n quarterly: 'quarter',\n 'semi-annual': 'semi-annual',\n year: 'year',\n yearly: 'year',\n lifetime: 'lifetime',\n unit: 'unit',\n};\n\nfunction cn(...classes: Array) {\n return classes.filter(Boolean).join(' ');\n}\n\nfunction formatCurrency(amount: number, currency: string) {\n const locale = currency === 'XOF' ? 'fr-FR' : undefined;\n const formattedAmount = amount.toLocaleString(locale, {\n minimumFractionDigits: currency === 'USD' ? 2 : 0,\n maximumFractionDigits: currency === 'USD' ? 2 : 0,\n });\n const displayCurrency = currency === 'XOF' ? 'F CFA' : currency;\n return `${formattedAmount} ${displayCurrency}`;\n}\n\nfunction formatBillingIntervalLabel(interval: string | null | undefined) {\n if (!interval) return 'month';\n return INTERVAL_LABELS[interval] ?? interval;\n}\n\nexport function PriceSelector({\n prices,\n selectedPriceId,\n onPriceSelect,\n currencyCode = 'XOF',\n embedded = false,\n label = 'Choose your billing cycle',\n saveLabel = 'Save',\n className,\n}: PriceSelectorProps) {\n const activePrices = prices.filter((price) => price.is_active !== false);\n\n if (activePrices.length <= 1) {\n return null;\n }\n\n const defaultPrice = activePrices.find((price) => price.is_default);\n const monthlyPrice = activePrices.find(\n (price) =>\n price.billing_interval === 'month' ||\n price.billing_interval === 'monthly',\n );\n\n const calculateSavings = (price: PriceOption) => {\n if (defaultPrice && defaultPrice.price_id !== price.price_id) {\n const savingsPercent = Math.round(\n ((defaultPrice.amount - price.amount) / defaultPrice.amount) * 100,\n );\n const savingsAmount = defaultPrice.amount - price.amount;\n\n if (savingsPercent > 0) {\n return { percent: savingsPercent, amount: savingsAmount };\n }\n }\n\n const isAnnualLike =\n price.billing_interval === 'year' || price.billing_interval === 'yearly';\n if (\n monthlyPrice &&\n isAnnualLike &&\n monthlyPrice.price_id !== price.price_id\n ) {\n const annualizedMonthly = monthlyPrice.amount * 12;\n const savingsAmount = annualizedMonthly - price.amount;\n if (savingsAmount > 0) {\n return {\n percent: Math.round((savingsAmount / annualizedMonthly) * 100),\n amount: savingsAmount,\n };\n }\n }\n\n return null;\n };\n\n return (\n
\n \n\n
\n {activePrices.map((price) => {\n const isSelected = price.price_id === selectedPriceId;\n const savings = calculateSavings(price);\n\n return (\n onPriceSelect(price.price_id)}\n className={cn(\n 'price-selector-option flex h-10 w-full items-center gap-2.5 rounded-sm bg-transparent px-3 text-left transition-colors duration-150 outline-none',\n isSelected\n ? 'price-selector-option-selected border border-[#56A5F9]'\n : 'border border-transparent',\n )}\n >\n \n {isSelected ? (\n
\n ) : null}\n
\n\n
\n \n {formatCurrency(price.amount, currencyCode)}\n \n \n / {formatBillingIntervalLabel(price.billing_interval)}\n \n {savings ? (\n \n {saveLabel} {savings.percent}%\n \n ) : null}\n
\n \n );\n })}\n
\n
\n );\n}\n" }, { "path": "components/lomi-ui/demo/price-selector-demo.tsx", "target": "components/lomi-ui/demo/price-selector-demo.tsx", "type": "registry:component", "content": "'use client';\n\nimport * as React from 'react';\nimport { LomiUiPreview } from '@/components/preview/lomi-ui-preview';\nimport { PriceSelector } from '@/components/lomi-ui/price-selector';\n\nexport function PriceSelectorDemo() {\n const [selectedPriceId, setSelectedPriceId] = React.useState('monthly');\n\n return (\n \n \n \n );\n}\n" } ] }