{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "radio-card",
  "type": "registry:ui",
  "title": "Radio Cards",
  "description": "Plan-style cards where one selection ring slides to whichever you choose.",
  "categories": [
    "selection"
  ],
  "dependencies": [
    "@base-ui/react",
    "motion"
  ],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json",
    "https://www.stealth.pm/r/motion.json",
    "https://www.stealth.pm/r/use-controllable-state.json"
  ],
  "files": [
    {
      "path": "components/ui/radio-card.tsx",
      "content": "\"use client\";\nimport { Radio } from \"@base-ui/react/radio\";\nimport { RadioGroup } from \"@base-ui/react/radio-group\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { createContext, use, useId } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { ease, spring } from \"@/lib/motion\";\nimport { useControllableState } from \"@/lib/use-controllable-state\";\n\nconst CardsContext = createContext<{ value: string; ring: string }>({ value: \"\", ring: \"\" });\n\nexport type RadioCardGroupProps = Omit<RadioGroup.Props<string>, \"className\" | \"value\" | \"defaultValue\" | \"onValueChange\"> & {\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  className?: string;\n};\n\nexport function RadioCardGroup({ value: valueProp, defaultValue = \"\", onValueChange, className, ...rest }: RadioCardGroupProps) {\n  const [value, setValue] = useControllableState({ value: valueProp, defaultValue, onChange: onValueChange });\n  // One selection ring for the whole group; it travels to whichever card is chosen.\n  const ring = useId();\n\n  return (\n    <CardsContext value={{ value, ring }}>\n      <RadioGroup\n        value={value}\n        onValueChange={(next) => setValue(next as string)}\n        className={cn(\"grid gap-2\", className)}\n        {...rest}\n      />\n    </CardsContext>\n  );\n}\n\nexport type RadioCardProps = Omit<Radio.Root.Props<string>, \"className\" | \"children\" | \"render\" | \"title\"> & {\n  value: string;\n  title: React.ReactNode;\n  description?: React.ReactNode;\n  /** Small pill beside the title, like “Most popular”. */\n  badge?: React.ReactNode;\n  /** Trailing content, usually the price. Right-aligned and never wrapped under the title. */\n  aside?: React.ReactNode;\n  className?: string;\n};\n\nexport function RadioCard({ value, title, description, badge, aside, disabled, className, ...rest }: RadioCardProps) {\n  const { value: selected, ring } = use(CardsContext);\n  const reduce = useReducedMotion();\n  const checked = selected === value;\n  const titleId = useId();\n  const descriptionId = useId();\n\n  return (\n    <Radio.Root\n      value={value}\n      disabled={disabled}\n      render={<div />}\n      aria-labelledby={titleId}\n      aria-describedby={description != null ? descriptionId : undefined}\n      // Read disabled and read-only from the primitive so a disabled group styles every card.\n      className={(state) =>\n        cn(\n          \"group/card relative flex min-w-0 select-none items-start gap-3 rounded-xl border p-3.5 text-left outline-none\",\n          \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3\",\n          \"transition-[background-color,border-color,scale] duration-150 ease-out-expo\",\n          checked ? \"border-transparent bg-hover\" : \"border-line-2 bg-raised\",\n          state.disabled\n            ? \"cursor-not-allowed opacity-50\"\n            : !state.readOnly && cn(\"motion-safe:active:scale-[0.99] active:duration-100\", !checked && \"hover:border-fg-4 hover:[&_[data-ring]]:border-fg-3\"),\n          className,\n        )\n      }\n      {...rest}\n    >\n      {checked && (\n        <motion.span\n          layoutId={ring}\n          aria-hidden\n          className=\"pointer-events-none absolute -inset-px border border-fg\"\n          // Radius in style so Motion corrects it while the ring stretches between cards of different heights.\n          style={{ borderRadius: 12 }}\n          transition={reduce ? { duration: 0 } : spring.snappy}\n        />\n      )}\n\n      <span className=\"flex h-5 shrink-0 items-center\">\n        <span\n          aria-hidden\n          data-ring\n          className={cn(\n            \"grid size-4 place-items-center rounded-full border transition-[background-color,border-color] duration-150\",\n            checked ? \"border-fg bg-fg\" : \"border-fg-4 bg-raised\",\n          )}\n        >\n          <motion.span\n            className=\"block size-1.5 rounded-full bg-frame\"\n            initial={false}\n            animate={checked ? { scale: 1, opacity: 1 } : { scale: 0.2, opacity: 0 }}\n            transition={\n              reduce\n                ? { opacity: { duration: 0.12 }, scale: { duration: 0 } }\n                : checked\n                  ? { ...spring.pop, opacity: { duration: 0.08 } }\n                  : { duration: 0.1, ease: ease.in }\n            }\n          />\n        </span>\n      </span>\n\n      <span className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n        <span className=\"flex min-w-0 items-center gap-1.5\">\n          <span id={titleId} className=\"truncate text-[13px] font-medium leading-5 tracking-[-0.005em] text-fg\">\n            {title}\n          </span>\n          {badge != null && (\n            <span className=\"shrink-0 rounded-full border border-line-2 px-1.5 text-[10.5px] leading-4 text-fg-2\">{badge}</span>\n          )}\n        </span>\n        {description != null && (\n          <span id={descriptionId} className=\"text-[12.5px] leading-[18px] text-fg-3\">\n            {description}\n          </span>\n        )}\n      </span>\n\n      {aside != null && <span className=\"shrink-0 text-right\">{aside}</span>}\n    </Radio.Root>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/radio-card"
}
