{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "radio-group",
  "title": "Radio Group",
  "description": "Radios whose dot scales out from the centre on a snappy spring, and radio cards whose selected border travels between cards. Base UI RadioGroup behaviour.",
  "dependencies": [
    "@base-ui/react"
  ],
  "registryDependencies": [
    "https://eased.dev/r/eased-tokens.json",
    "https://eased.dev/r/eased-indicator.json"
  ],
  "files": [
    {
      "path": "src/registry/eased/radio-group/radio-group.tsx",
      "content": "\"use client\";\n\nimport { Radio as BaseRadio } from \"@base-ui/react/radio\";\nimport { RadioGroup as BaseRadioGroup } from \"@base-ui/react/radio-group\";\nimport { useId, type ReactNode } from \"react\";\nimport { Indicator, IndicatorGroup } from \"../_primitives/indicator/indicator\";\n\ntype GroupProps<V extends string> = {\n  /** The selected value (controlled). */\n  value?: V;\n  /** The initially selected value (uncontrolled). */\n  defaultValue?: V;\n  onValueChange?: (value: V) => void;\n  /** Submits the value with a form under this name. */\n  name?: string;\n  disabled?: boolean;\n  required?: boolean;\n  /** Accessible name, e.g. \"Delivery\". Or point `aria-labelledby` at a visible label. */\n  \"aria-label\"?: string;\n  \"aria-labelledby\"?: string;\n  className?: string;\n  children: ReactNode;\n};\n\nexport type RadioGroupProps<V extends string> = GroupProps<V>;\n\n/**\n * A list of radios. Base UI's RadioGroup supplies the behaviour: one tab stop, arrow keys\n * move and select, form submission and Field validation.\n */\nexport function RadioGroup<V extends string>({ className = \"\", onValueChange, children, ...props }: RadioGroupProps<V>) {\n  return (\n    <BaseRadioGroup<V>\n      {...props}\n      onValueChange={(v) => onValueChange?.(v)}\n      className={`flex flex-col gap-1 ${className}`}\n    >\n      {children}\n    </BaseRadioGroup>\n  );\n}\n\n/** The 16px control itself: a ring that fills, and a dot that scales out from the centre. */\nfunction RadioControl({ className = \"\" }: { className?: string }) {\n  return (\n    <span\n      aria-hidden\n      className={`flex size-4 shrink-0 items-center justify-center rounded-pill border border-line-strong bg-surface transition-colors duration-150 ease-standard group-data-checked:border-brand group-data-checked:bg-brand group-data-disabled:border-line group-data-disabled:bg-surface-sunken group-hover-fine:group-data-unchecked:border-text-muted ${className}`}\n    >\n      <span className=\"size-1.5 scale-50 rounded-pill bg-text-on-brand opacity-0 transition-[scale,opacity] duration-(--eased-duration-instant) ease-exit group-data-checked:scale-100 group-data-checked:opacity-100 group-data-checked:duration-(--eased-duration-spring-snappy) group-data-checked:ease-spring-snappy group-data-disabled:bg-text-disabled reduced:scale-100 reduced:duration-(--eased-duration-instant)!\" />\n    </span>\n  );\n}\n\nexport type RadioProps<V extends string> = {\n  value: V;\n  /** The label. Clicking it selects the radio. */\n  children: ReactNode;\n  /** Muted text under the label. */\n  description?: ReactNode;\n  disabled?: boolean;\n  className?: string;\n};\n\n/** One radio with its label (and optional description), aligned to the label's first line. */\nexport function Radio<V extends string>({ value, children, description, disabled, className = \"\" }: RadioProps<V>) {\n  const id = useId();\n  return (\n    <BaseRadio.Root\n      value={value}\n      disabled={disabled}\n      aria-labelledby={`${id}-label`}\n      aria-describedby={description ? `${id}-desc` : undefined}\n      className={(state) =>\n        `group flex min-w-0 items-start gap-2.5 rounded-control-xs py-1.5 text-left select-none focus-ring ${\n          state.disabled ? \"cursor-not-allowed\" : \"cursor-pointer\"\n        } ${className}`\n      }\n    >\n      {/* 16px control centred on the 14px label's 20px first line. */}\n      <span className=\"flex h-5 items-center\">\n        <RadioControl />\n      </span>\n      <span className=\"flex min-w-0 flex-col gap-0.5\">\n        <span id={`${id}-label`} className=\"text-ui text-text group-data-disabled:text-text-disabled\">\n          {children}\n        </span>\n        {description && (\n          <span id={`${id}-desc`} className=\"text-ui-sm text-text-muted text-pretty\">\n            {description}\n          </span>\n        )}\n      </span>\n    </BaseRadio.Root>\n  );\n}\n\nexport type RadioCardsProps<V extends string> = GroupProps<V> & {\n  /** Columns at full width; cards stack to one column under 360px of container. Default 1. */\n  columns?: 1 | 2 | 3;\n};\n\nconst COLUMNS = { 1: \"grid-cols-1\", 2: \"grid-cols-1 @min-[22rem]:grid-cols-2\", 3: \"grid-cols-1 @min-[30rem]:grid-cols-3\" } as const;\n\n/**\n * Radio cards: the selected card's border is one highlight that travels between cards\n * on the snappy spring. Cards in a row share a height (`auto-rows-fr`), so the ring only\n * ever translates and never distorts.\n */\nexport function RadioCards<V extends string>({ className = \"\", columns = 1, onValueChange, children, ...props }: RadioCardsProps<V>) {\n  return (\n    <div className=\"@container w-full min-w-0\">\n      <BaseRadioGroup<V>\n        {...props}\n        onValueChange={(v) => onValueChange?.(v)}\n        className={`grid auto-rows-fr gap-2 ${COLUMNS[columns]} ${className}`}\n      >\n        <IndicatorGroup>{children}</IndicatorGroup>\n      </BaseRadioGroup>\n    </div>\n  );\n}\n\nexport type RadioCardProps<V extends string> = {\n  value: V;\n  title: ReactNode;\n  description?: ReactNode;\n  /** Right-aligned detail on the title row, e.g. a price. */\n  meta?: ReactNode;\n  /** A 16px icon before the title. */\n  icon?: ReactNode;\n  disabled?: boolean;\n  className?: string;\n};\n\n/** One card. The whole card is the hit target; the radio dot sits top-right as the affordance. */\nexport function RadioCard<V extends string>({ value, title, description, meta, icon, disabled, className = \"\" }: RadioCardProps<V>) {\n  const id = useId();\n  return (\n    <BaseRadio.Root\n      value={value}\n      disabled={disabled}\n      aria-labelledby={`${id}-title`}\n      aria-describedby={description ? `${id}-desc` : undefined}\n      render={(htmlProps, state) => (\n        <span {...htmlProps}>\n          {state.checked && <Indicator className=\"-inset-px rounded-[calc(var(--eased-radius-control)+1px)] border border-brand-line bg-brand-subtle\" />}\n          <span className=\"relative flex min-w-0 items-start gap-2.5\">\n            {icon && <span className=\"flex h-5 shrink-0 items-center text-text-secondary [&_svg]:size-4\">{icon}</span>}\n            <span className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n              <span className=\"flex min-w-0 items-baseline gap-2\">\n                <span id={`${id}-title`} className=\"min-w-0 flex-1 truncate text-ui font-medium text-text group-data-disabled:text-text-disabled\">\n                  {title}\n                </span>\n                {meta && <span className=\"shrink-0 text-ui-sm text-text-secondary tabular-nums\">{meta}</span>}\n              </span>\n              {description && (\n                <span id={`${id}-desc`} className=\"text-ui-sm text-text-muted text-pretty\">\n                  {description}\n                </span>\n              )}\n            </span>\n            <span className=\"flex h-5 items-center\">\n              <RadioControl />\n            </span>\n          </span>\n        </span>\n      )}\n      className={(state) =>\n        `group relative flex min-w-0 flex-col justify-center rounded-control border border-line-subtle bg-surface p-3 text-left select-none transition-colors duration-150 ease-standard focus-ring ${\n          state.disabled\n            ? \"cursor-not-allowed bg-surface-sunken\"\n            : state.checked\n              ? \"cursor-default\"\n              : \"cursor-pointer hover-fine:border-line hover-fine:bg-surface-hover\"\n        } ${className}`\n      }\n    />\n  );\n}\n",
      "type": "registry:ui",
      "target": "@components/eased/radio-group/radio-group.tsx"
    }
  ],
  "docs": "Import RadioGroup, Radio, RadioCards and RadioCard from components/eased/radio-group/radio-group. The Eased tokens were merged into your global CSS; nothing to import. Dark mode follows .dark or data-theme=\"dark\".",
  "type": "registry:ui"
}