{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"price-toggle","type":"registry:ui","title":"Price Toggle","description":"A monthly / yearly pricing switch: the thumb slides between periods, the saving badge pops, and each digit of the price rolls to its new value, up when it rises and down when it drops.","dependencies":["motion"],"registryDependencies":["utils"],"files":[{"path":"registry/ui/price-toggle.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { AnimatePresence, MotionConfig, motion } from \"motion/react\";\nimport { useId, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type Billing = \"monthly\" | \"yearly\";\n\nexport interface PriceToggleProps {\n  /** Price per month when billed monthly. */\n  monthly: number;\n  /** Price per month when billed yearly. */\n  yearly: number;\n  currency?: string;\n  /** Controlled billing period. */\n  value?: Billing;\n  defaultValue?: Billing;\n  onChange?: (billing: Billing) => void;\n  /** Badge on the yearly option. Defaults to the computed saving, e.g. \"−20%\". */\n  badge?: string;\n  className?: string;\n}\n\n/**\n * A monthly / yearly switch for pricing. The thumb slides between the two\n * periods and every digit of the price rolls to its new value, up when the\n * price rises and down when it drops.\n */\nexport function PriceToggle({\n  monthly,\n  yearly,\n  currency = \"$\",\n  value,\n  defaultValue = \"monthly\",\n  onChange,\n  badge,\n  className,\n}: PriceToggleProps) {\n  const thumbId = useId();\n  const [inner, setInner] = useState<Billing>(defaultValue);\n  const billing = value ?? inner;\n  const price = billing === \"yearly\" ? yearly : monthly;\n  // Remember the last price to know which way the digits should roll.\n  const [last, setLast] = useState({ price, dir: 1 });\n  if (last.price !== price) setLast({ price, dir: price > last.price ? 1 : -1 });\n  const dir = last.price !== price ? (price > last.price ? 1 : -1) : last.dir;\n  const saving = badge ?? (monthly > 0 ? `−${Math.round((1 - yearly / monthly) * 100)}%` : \"\");\n  const digits = String(price).split(\"\");\n\n  const set = (b: Billing) => {\n    if (value === undefined) setInner(b);\n    onChange?.(b);\n  };\n\n  return (\n    <MotionConfig reducedMotion=\"user\">\n      <div className={cn(\"grid justify-items-center gap-5\", className)}>\n        <div role=\"radiogroup\" aria-label=\"Billing period\" className=\"relative flex rounded-full border border-border bg-muted/60 p-1\">\n          {([\"monthly\", \"yearly\"] as const).map((b) => (\n            <button\n              key={b}\n              type=\"button\"\n              role=\"radio\"\n              aria-checked={billing === b}\n              onClick={() => set(b)}\n              onKeyDown={(e) => {\n                if ([\"ArrowLeft\", \"ArrowRight\", \"ArrowUp\", \"ArrowDown\"].includes(e.key)) {\n                  e.preventDefault();\n                  set(b === \"monthly\" ? \"yearly\" : \"monthly\");\n                  (e.currentTarget.parentElement?.querySelector(`[aria-checked=\"false\"]`) as HTMLElement | null)?.focus();\n                }\n              }}\n              tabIndex={billing === b ? 0 : -1}\n              className={cn(\n                \"relative z-10 flex h-8 items-center gap-1.5 rounded-full px-4 text-sm font-medium capitalize outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring\",\n                billing === b ? \"text-foreground\" : \"text-muted-foreground hover:text-foreground\",\n              )}\n            >\n              {billing === b && (\n                <motion.span\n                  layoutId={thumbId}\n                  className=\"absolute inset-0 -z-10 rounded-full bg-card shadow-sm\"\n                  transition={{ type: \"spring\", bounce: 0.25, duration: 0.4 }}\n                />\n              )}\n              {b}\n              {b === \"yearly\" && saving && (\n                <motion.span\n                  className=\"rounded-full bg-primary px-1.5 py-0.5 text-[10px] font-bold text-primary-foreground\"\n                  animate={billing === \"yearly\" ? { scale: [1, 1.25, 1], rotate: [0, -8, 0] } : { scale: 1, rotate: 0 }}\n                  transition={{ duration: 0.4 }}\n                >\n                  {saving}\n                </motion.span>\n              )}\n            </button>\n          ))}\n        </div>\n\n        <div className=\"flex items-start gap-1 text-foreground\">\n          <span className=\"mt-2 text-2xl font-semibold text-muted-foreground\">{currency}</span>\n          <span className=\"sr-only\">\n            {currency}\n            {price} per month, billed {billing}\n          </span>\n          <span aria-hidden className=\"flex overflow-hidden text-6xl font-bold tabular-nums leading-none tracking-tight\">\n            {digits.map((d, i) => (\n              // Keyed by place value from the right, so only changed digits roll.\n              <span key={digits.length - i} className=\"relative inline-block\">\n                <AnimatePresence initial={false} mode=\"popLayout\" custom={dir}>\n                  <motion.span\n                    key={d}\n                    custom={dir}\n                    className=\"inline-block\"\n                    variants={{\n                      enter: (s: number) => ({ y: `${s * 100}%`, opacity: 0, filter: \"blur(4px)\" }),\n                      center: { y: \"0%\", opacity: 1, filter: \"blur(0px)\" },\n                      exit: (s: number) => ({ y: `${s * -100}%`, opacity: 0, filter: \"blur(4px)\" }),\n                    }}\n                    initial=\"enter\"\n                    animate=\"center\"\n                    exit=\"exit\"\n                    transition={{ type: \"spring\", bounce: 0.2, duration: 0.45, delay: i * 0.04 }}\n                  >\n                    {d}\n                  </motion.span>\n                </AnimatePresence>\n              </span>\n            ))}\n          </span>\n          <span className=\"mt-auto pb-1.5 text-sm text-muted-foreground\">/mo</span>\n        </div>\n        <AnimatePresence mode=\"wait\" initial={false}>\n          <motion.p\n            key={billing}\n            className=\"-mt-2 text-xs text-muted-foreground\"\n            initial={{ opacity: 0, y: 4 }}\n            animate={{ opacity: 1, y: 0 }}\n            exit={{ opacity: 0, y: -4 }}\n            transition={{ duration: 0.18 }}\n          >\n            {billing === \"yearly\" ? `${currency}${yearly * 12} billed once a year` : \"Billed every month, cancel anytime\"}\n          </motion.p>\n        </AnimatePresence>\n      </div>\n    </MotionConfig>\n  );\n}\n"}]}