{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "radio-group-motion",
  "title": "Group-motion radio",
  "description": "Styled radio: the Group-motion set. 5 decorative radio variations (SlideRadio, PopRadio, FadeRadio, SpringRadio, MorphRadio) on the ai2 token system, powered by framer-motion, reduced-motion aware and sized sm to xl. Part of the free styled layer.",
  "dependencies": [
    "motion@^12.42.2"
  ],
  "registryDependencies": [
    "@ai2/tokens"
  ],
  "files": [
    {
      "path": "registry/ai2/styled/radio-group-motion.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { motion, useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* Motion radio-group family: 5 selection groups where the selection marker is\n   carried between options with a framer layoutId. The variants differ in the\n   character of the transition: slide, pop, cross-fade, spring and shape change. The\n   layoutId is unique per instance via useId (so that 25 examples on the same page\n   do not animate across each other). role=\"radiogroup\" + role=\"radio\", roving\n   tabindex, arrow-key navigation and selection. Color comes ONLY from tokens, via\n   alpha color-mix. Under reduced-motion the marker jumps instantly. Renders with no\n   props too. */\n\nexport type StyledSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\n\ntype RadioOption = { value: string; label: React.ReactNode }\n\ninterface RadioGroupProps {\n  className?: string\n  size?: StyledSize\n  options?: RadioOption[]\n  value?: string\n  defaultValue?: string\n  onValueChange?: (v: string) => void\n}\n\nconst DEFAULT_OPTIONS: RadioOption[] = [\n  { value: \"a\", label: \"Option A\" },\n  { value: \"b\", label: \"Option B\" },\n  { value: \"c\", label: \"Option C\" },\n]\n\nconst textSize: Record<StyledSize, string> = {\n  sm: \"text-xs\",\n  md: \"text-sm\",\n  lg: \"text-base\",\n  xl: \"text-lg\",\n}\n\nconst padSize: Record<StyledSize, string> = {\n  sm: \"px-2.5 py-1\",\n  md: \"px-3 py-1.5\",\n  lg: \"px-4 py-2\",\n  xl: \"px-5 py-2.5\",\n}\n\nfunction useRadioGroup(props: Pick<RadioGroupProps, \"value\" | \"defaultValue\" | \"onValueChange\" | \"options\">) {\n  const { value, defaultValue, onValueChange, options } = props\n  const opts = options && options.length > 0 ? options : DEFAULT_OPTIONS\n  const reduce = useReducedMotion()\n  const [internal, setInternal] = React.useState(defaultValue ?? opts[0]?.value)\n  const selected = value ?? internal\n  const select = React.useCallback(\n    (v: string) => {\n      if (value === undefined) setInternal(v)\n      onValueChange?.(v)\n    },\n    [value, onValueChange]\n  )\n  return { opts, selected, select, reduce }\n}\n\n/* Arrow-key navigation + selection (together with the roving tabindex). */\nfunction useRadioKeys(opts: RadioOption[], select: (v: string) => void) {\n  const refs = React.useRef<(HTMLButtonElement | null)[]>([])\n  const setRef = (i: number) => (el: HTMLButtonElement | null) => {\n    refs.current[i] = el\n  }\n  const onKeyDown = (i: number) => (e: React.KeyboardEvent) => {\n    const last = opts.length - 1\n    let next = -1\n    if (e.key === \"ArrowRight\" || e.key === \"ArrowDown\") next = i === last ? 0 : i + 1\n    else if (e.key === \"ArrowLeft\" || e.key === \"ArrowUp\") next = i === 0 ? last : i - 1\n    if (next < 0) return\n    e.preventDefault()\n    const target = opts[next]\n    if (!target) return\n    select(target.value)\n    refs.current[next]?.focus()\n  }\n  return { setRef, onKeyDown }\n}\n\nconst focusRing =\n  \"outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background\"\n\ntype IndicatorMotion = {\n  initial?: Record<string, number>\n  animate?: Record<string, number>\n  exit?: Record<string, number>\n}\n\n/* Shared shell: a horizontal option row plus an indicator carried by layoutId. */\nfunction MotionShell({\n  props,\n  transition,\n  indicatorClass,\n  indicatorMotion,\n  activeText = \"text-primary-foreground\",\n  trackClass = \"bg-muted\",\n}: {\n  props: RadioGroupProps\n  transition: Record<string, unknown>\n  indicatorClass: string\n  indicatorMotion?: IndicatorMotion\n  activeText?: string\n  trackClass?: string\n}) {\n  const { className, size = \"md\", options, value, defaultValue, onValueChange } = props\n  const { opts, selected, select, reduce } = useRadioGroup({ options, value, defaultValue, onValueChange })\n  const { setRef, onKeyDown } = useRadioKeys(opts, select)\n  const groupId = React.useId()\n  const activeIndex = opts.findIndex((o) => o.value === selected)\n  const t = reduce ? { duration: 0 } : transition\n  const anim = reduce ? undefined : indicatorMotion\n\n  return (\n    <div\n      data-slot=\"styled-radio-group\"\n      role=\"radiogroup\"\n      aria-label=\"Choose an option\"\n      className={cn(\"inline-flex items-center gap-1 rounded-lg p-1\", trackClass, className)}\n    >\n      {opts.map((opt, i) => {\n        const active = opt.value === selected\n        const roving = active || (activeIndex < 0 && i === 0)\n        return (\n          <button\n            key={opt.value}\n            ref={setRef(i)}\n            type=\"button\"\n            role=\"radio\"\n            aria-checked={active}\n            tabIndex={roving ? 0 : -1}\n            onKeyDown={onKeyDown(i)}\n            onClick={() => select(opt.value)}\n            data-slot=\"styled-radio-item\"\n            className={cn(\n              \"relative z-10 cursor-pointer rounded-md font-medium transition-colors\",\n              focusRing,\n              textSize[size],\n              padSize[size],\n              active ? activeText : \"text-muted-foreground hover:text-foreground\"\n            )}\n          >\n            {active ? (\n              <motion.span\n                layoutId={`motion-radio-${groupId}`}\n                transition={t}\n                initial={anim?.initial}\n                animate={anim?.animate}\n                className={cn(\"absolute inset-0 -z-10\", indicatorClass)}\n              />\n            ) : null}\n            <span className=\"relative\">{opt.label}</span>\n          </button>\n        )\n      })}\n    </div>\n  )\n}\n\n/* Slide: the marker slides between the options with a plain tween. */\nexport function SlideRadio(props: RadioGroupProps) {\n  return (\n    <MotionShell\n      props={props}\n      transition={{ type: \"tween\" as const, duration: 0.28, ease: \"easeInOut\" }}\n      indicatorClass=\"rounded-md bg-primary shadow-sm\"\n    />\n  )\n}\n\n/* Pop: the indicator does a short scale pop while it travels. */\nexport function PopRadio(props: RadioGroupProps) {\n  return (\n    <MotionShell\n      props={props}\n      transition={{ type: \"spring\" as const, stiffness: 700, damping: 22 }}\n      indicatorMotion={{ initial: { scale: 0.7, opacity: 0.6 }, animate: { scale: 1, opacity: 1 } }}\n      indicatorClass=\"rounded-md bg-primary shadow-sm\"\n    />\n  )\n}\n\n/* Fade: isaret tasinirken capraz gecisle solar ve belirir. */\nexport function FadeRadio(props: RadioGroupProps) {\n  return (\n    <MotionShell\n      props={props}\n      transition={{ type: \"tween\" as const, duration: 0.32, ease: \"easeOut\" }}\n      indicatorMotion={{ initial: { opacity: 0 }, animate: { opacity: 1 } }}\n      activeText=\"text-primary\"\n      indicatorClass=\"rounded-md border border-primary bg-[color-mix(in_oklab,var(--color-primary)_14%,transparent)]\"\n    />\n  )\n}\n\n/* Spring: yayli, hafif zipli tasima. */\nexport function SpringRadio(props: RadioGroupProps) {\n  return (\n    <MotionShell\n      props={props}\n      transition={{ type: \"spring\" as const, stiffness: 320, damping: 16 }}\n      indicatorClass=\"rounded-full bg-primary shadow-sm\"\n    />\n  )\n}\n\n/* Morph: isaret tasinirken alt cizgiden dolu pill'e sekil degistirir. */\nexport function MorphRadio(props: RadioGroupProps) {\n  return (\n    <MotionShell\n      props={props}\n      transition={{ type: \"spring\" as const, stiffness: 420, damping: 30 }}\n      indicatorMotion={{ initial: { scaleY: 0.18, opacity: 0.5 }, animate: { scaleY: 1, opacity: 1 } }}\n      trackClass=\"bg-transparent\"\n      indicatorClass=\"rounded-full border border-primary bg-[color-mix(in_oklab,var(--color-primary)_18%,transparent)]\"\n      activeText=\"text-primary\"\n    />\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ui/radio-group-motion.tsx"
    }
  ],
  "categories": [
    "styled",
    "radio"
  ],
  "type": "registry:component"
}