{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pricing-toggle",
  "title": "Pricing Toggle",
  "description": "Switch billing periods while keeping prices, billing context, and savings labels in sync.",
  "dependencies": [
    "class-variance-authority"
  ],
  "registryDependencies": [
    "toggle",
    "https://uaiblocks.dev/r/uai-theme.json",
    "https://uaiblocks.dev/r/uai-utils.json"
  ],
  "files": [
    {
      "path": "src/registry/uai/components/pricing-toggle.tsx",
      "content": "\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport {\n  type ComponentProps,\n  createContext,\n  type KeyboardEvent,\n  type ReactNode,\n  useContext,\n  useEffect,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { toggleVariants } from \"@/components/ui/toggle\";\nimport { cn } from \"@/lib/uai-utils\";\n\n/**\n * Single-choice segments are plain radio buttons styled like ToggleGroupItem: ToggleGroup's value\n * is a string on Radix but an array on Base UI, so the group's selection stays in uai state.\n */\nconst segmentClass = \"w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10\";\n\nexport const PRICING_TOGGLE_VARIANTS = [\"segmented\", \"pill\", \"compact\"] as const;\nexport type PricingToggleVariant = (typeof PRICING_TOGGLE_VARIANTS)[number];\nexport type PricingToggleProps = Omit<ComponentProps<\"div\">, \"defaultValue\" | \"onChange\"> & {\n  variant?: PricingToggleVariant;\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n};\ntype PricingContext = {\n  variant: PricingToggleVariant;\n  value: string;\n  select: (value: string) => void;\n};\nconst Context = createContext<PricingContext | null>(null);\nfunction usePricing(part: string) {\n  const context = useContext(Context);\n  if (!context) throw new Error(`${part} must be used within PricingToggle`);\n  return context;\n}\n\nconst useIsomorphicLayoutEffect = typeof window === \"undefined\" ? useEffect : useLayoutEffect;\nconst pricingToggleListVariants = cva(\n  \"relative isolate inline-flex max-w-full items-center justify-self-start gap-0.5\",\n  {\n    variants: {\n      variant: {\n        segmented: \"rounded-[10px] bg-background p-0.75 shadow-[inset_0_0_0_1px_var(--border)]\",\n        pill: \"rounded-full bg-card p-0.75 shadow-[inset_0_0_0_1px_var(--border)]\",\n        compact: \"rounded-lg bg-transparent p-0\",\n      },\n    },\n  },\n);\nconst pricingToggleThumbVariants = cva(\n  \"pointer-events-none absolute top-0 left-0 -z-1 opacity-0 [transition:transform_220ms_cubic-bezier(0.23,1,0.32,1),width_220ms_cubic-bezier(0.23,1,0.32,1),opacity_120ms_ease-out] group-data-ready/list:opacity-100 motion-reduce:transition-none\",\n  {\n    variants: {\n      variant: {\n        segmented:\n          \"rounded-[7px] bg-card shadow-[0_0_0_1px_var(--border),0_1px_2px_oklch(0_0_0/0.08)]\",\n        pill: \"rounded-full bg-accent\",\n        compact: \"rounded-md bg-card shadow-[0_0_0_1px_var(--border)]\",\n      },\n    },\n  },\n);\nconst pricingToggleOptionVariants = cva(\n  \"inline-flex min-w-0 cursor-pointer items-center justify-start gap-1.5 border-0 bg-transparent font-medium whitespace-nowrap text-subtle-foreground [transition:color_120ms_ease-out,background-color_120ms_ease-out,scale_140ms_cubic-bezier(0.23,1,0.32,1)] hover:bg-transparent hover:text-foreground focus-visible:ring-0 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ring focus-visible:outline-solid active:scale-[0.97] aria-checked:text-foreground data-[state=on]:text-foreground motion-reduce:transition-none motion-reduce:active:scale-100\",\n  {\n    variants: {\n      // The thumb paints the checked option once measured; until then the option paints itself.\n      variant: {\n        segmented:\n          \"h-7 rounded-[7px] px-3 text-[13px]/[18px] data-[state=on]:bg-card group-data-ready/list:data-[state=on]:bg-transparent\",\n        pill: \"h-7 rounded-full px-3.5 text-[13px]/[18px] data-[state=on]:bg-accent group-data-ready/list:data-[state=on]:bg-transparent\",\n        compact:\n          \"h-6 rounded-md px-2 text-xs/[18px] data-[state=on]:bg-card group-data-ready/list:data-[state=on]:bg-transparent\",\n      },\n    },\n  },\n);\n\n/** Reads the selected billing period inside PricingToggle. */\nexport function usePricingPeriod() {\n  return usePricing(\"usePricingPeriod\").value;\n}\n\nexport function PricingToggle({\n  variant = \"segmented\",\n  value,\n  defaultValue = \"\",\n  onValueChange,\n  children,\n  className,\n  ...props\n}: PricingToggleProps) {\n  const [internal, setInternal] = useState(defaultValue);\n  const current = value ?? internal;\n  const select = (next: string) => {\n    if (next === current) return;\n    if (value === undefined) setInternal(next);\n    onValueChange?.(next);\n  };\n  return (\n    <Context.Provider value={{ variant, value: current, select }}>\n      <div\n        data-slot=\"pricing-toggle\"\n        data-variant={variant}\n        className={cn(\n          \"grid min-w-0 text-[13px]/[18px] text-foreground\",\n          variant === \"compact\" ? \"gap-3\" : \"gap-4\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n      </div>\n    </Context.Provider>\n  );\n}\n\nconst MOVES: Record<string, (index: number, count: number) => number> = {\n  ArrowRight: (index, count) => (index + 1) % count,\n  ArrowDown: (index, count) => (index + 1) % count,\n  ArrowLeft: (index, count) => (index - 1 + count) % count,\n  ArrowUp: (index, count) => (index - 1 + count) % count,\n  Home: () => 0,\n  End: (_index, count) => count - 1,\n};\n\nexport function PricingToggleList({\n  className,\n  onKeyDown,\n  children,\n  ...props\n}: Omit<ComponentProps<\"div\">, \"defaultValue\" | \"dir\">) {\n  const context = usePricing(\"PricingToggleList\");\n  const ref = useRef<HTMLDivElement>(null);\n  const [thumb, setThumb] = useState<{ x: number; y: number; width: number; height: number }>();\n  useIsomorphicLayoutEffect(() => {\n    const list = ref.current;\n    if (!list) return;\n    const measure = () => {\n      const checked = list.querySelector<HTMLElement>(\"[role=radio][aria-checked=true]\");\n      if (!checked || checked.offsetWidth === 0) return setThumb(undefined);\n      setThumb({\n        x: checked.offsetLeft,\n        y: checked.offsetTop,\n        width: checked.offsetWidth,\n        height: checked.offsetHeight,\n      });\n    };\n    measure();\n    if (typeof ResizeObserver !== \"function\") return;\n    const observer = new ResizeObserver(measure);\n    observer.observe(list);\n    return () => observer.disconnect();\n  }, [context.value]);\n  // Arrows move focus and selection together, as in an APG radio group.\n  const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n    onKeyDown?.(event);\n    const move = MOVES[event.key];\n    if (event.defaultPrevented || !move || !ref.current) return;\n    const options = Array.from(\n      ref.current.querySelectorAll<HTMLButtonElement>(\"[role=radio]:not(:disabled)\"),\n    );\n    if (options.length === 0) return;\n    event.preventDefault();\n    const index = options.indexOf(document.activeElement as HTMLButtonElement);\n    const next = options[move(Math.max(index, 0), options.length)];\n    next?.focus();\n    if (next?.dataset.value) context.select(next.dataset.value);\n  };\n  return (\n    <div\n      role=\"radiogroup\"\n      data-slot=\"pricing-toggle-list\"\n      className={cn(\n        \"group/list\",\n        pricingToggleListVariants({ variant: context.variant }),\n        className,\n      )}\n      {...props}\n      ref={ref}\n      data-ready={thumb ? \"\" : undefined}\n      onKeyDown={handleKeyDown}\n    >\n      <span\n        aria-hidden=\"true\"\n        data-slot=\"pricing-toggle-thumb\"\n        className={pricingToggleThumbVariants({ variant: context.variant })}\n        style={{\n          width: thumb?.width ?? 0,\n          height: thumb?.height ?? 0,\n          transform: thumb ? `translate(${thumb.x}px, ${thumb.y}px)` : undefined,\n        }}\n      />\n      {children}\n    </div>\n  );\n}\n\nexport function PricingToggleOption({\n  value,\n  onClick,\n  children,\n  className,\n  ...props\n}: Omit<ComponentProps<\"button\">, \"value\"> & { value: string }) {\n  const context = usePricing(\"PricingToggleOption\");\n  const checked = context.value === value;\n  return (\n    // biome-ignore lint/a11y/useSemanticElements: a segment button in a radio group; ToggleGroupItem renders the same role=\"radio\" button.\n    <button\n      role=\"radio\"\n      aria-checked={checked}\n      data-slot=\"pricing-toggle-option\"\n      data-state={checked ? \"on\" : \"off\"}\n      className={cn(\n        toggleVariants(),\n        segmentClass,\n        pricingToggleOptionVariants({ variant: context.variant }),\n        className,\n      )}\n      {...props}\n      type=\"button\"\n      data-value={value}\n      tabIndex={checked || context.value === \"\" ? 0 : -1}\n      onClick={(event) => {\n        onClick?.(event);\n        if (!event.defaultPrevented) context.select(value);\n      }}\n    >\n      {children}\n    </button>\n  );\n}\n\nexport function PricingToggleSavings({ className, ...props }: ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"pricing-toggle-savings\"\n      className={cn(\n        \"inline-flex h-[18px] items-center rounded-full bg-success/14 px-1.5 text-[11px]/4 font-medium text-success-ink tabular-nums\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function PricingTogglePrice({\n  period,\n  children,\n  className,\n  ...props\n}: ComponentProps<\"span\"> & { period: string; children?: ReactNode }) {\n  const context = usePricing(\"PricingTogglePrice\");\n  if (context.value !== period) return null;\n  return (\n    <span\n      data-slot=\"pricing-toggle-price\"\n      // Each period mounts its own price, so the swap fades up on change.\n      className={cn(\n        \"inline-block animate-in duration-200 ease-out-quint tabular-nums fade-in-0 slide-in-from-bottom-1 motion-reduce:animate-none\",\n        className,\n      )}\n      {...props}\n      data-period={period}\n    >\n      {children}\n    </span>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/uai/pricing-toggle.tsx"
    }
  ],
  "categories": [
    "marketing",
    "pricing"
  ],
  "type": "registry:ui"
}