{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "radio-group-image",
  "title": "Group-image radio",
  "description": "Styled radio: the Group-image set. 5 decorative radio variations (SwatchRadio, ThumbRadio, ColorRadio, AvatarRadio, TileRadio) 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",
    "lucide-react@^1.23.0"
  ],
  "registryDependencies": [
    "@ai2/tokens"
  ],
  "files": [
    {
      "path": "registry/ai2/styled/radio-group-image.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Check } from \"lucide-react\"\nimport { motion, useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* Image radio-group family: 5 visual selection tiles. There are NO remote image\n   URLs - every surface is a token fill or an inline SVG. The variants: color\n   swatches, small preview tiles, labelled color chips, initial-based avatars and\n   large tiles. The selection is indicated not by color alone but also with a\n   checkmark. role=\"radiogroup\" + role=\"radio\", roving tabindex, arrow-key\n   navigation and selection. Color comes ONLY from tokens, via alpha color-mix.\n   Under reduced-motion the checkmark appears instantly. Renders with no props\n   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\n/* Swatch and avatar box: all of them stay above 24px. */\nconst swatchSize: Record<StyledSize, string> = {\n  sm: \"size-7\",\n  md: \"size-8\",\n  lg: \"size-9\",\n  xl: \"size-10\",\n}\n\nconst tileSize: Record<StyledSize, string> = {\n  sm: \"size-14\",\n  md: \"size-16\",\n  lg: \"size-20\",\n  xl: \"size-24\",\n}\n\n/* Tile media strip: the width follows the tile scale, the height a fixed ratio. */\nconst tileMedia: Record<StyledSize, string> = {\n  sm: \"h-10 min-w-24\",\n  md: \"h-12 min-w-28\",\n  lg: \"h-14 min-w-32\",\n  xl: \"h-16 min-w-36\",\n}\n\nconst checkSize: Record<StyledSize, string> = {\n  sm: \"size-3\",\n  md: \"size-3.5\",\n  lg: \"size-4\",\n  xl: \"size-4\",\n}\n\n/* Semantic tone surfaces - no hex, tokens only. */\nconst TONE_SURFACE = [\"bg-brand\", \"bg-info\", \"bg-success\", \"bg-warning\", \"bg-danger\"]\nconst TONE_ON = [\n  \"text-brand-foreground\",\n  \"text-info-foreground\",\n  \"text-success-foreground\",\n  \"text-warning-foreground\",\n  \"text-danger-foreground\",\n]\nconst TONE_NAME = [\"brand\", \"info\", \"success\", \"warning\", \"danger\"]\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  const spring = reduce\n    ? { duration: 0 }\n    : ({ type: \"spring\" as const, stiffness: 520, damping: 30 })\n  return { opts, selected, select, reduce, spring }\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\n/* Secili yuzeyde beliren onay isareti. */\nfunction CheckMark({ active, spring, className }: { active: boolean; spring: Record<string, unknown>; className?: string }) {\n  return (\n    <motion.span\n      initial={false}\n      animate={{ scale: active ? 1 : 0, opacity: active ? 1 : 0 }}\n      transition={spring}\n      className={cn(\"pointer-events-none inline-flex items-center justify-center\", className)}\n      aria-hidden=\"true\"\n    >\n      <Check className=\"size-full\" strokeWidth={3} />\n    </motion.span>\n  )\n}\n\n/* Etiketten deterministik bas harf. */\nfunction initial(label: React.ReactNode, i: number) {\n  if (typeof label === \"string\" && label.trim().length > 0) return label.trim().charAt(0).toUpperCase()\n  if (typeof label === \"number\") return String(label)\n  return String(i + 1)\n}\n\n/* An inline preview graphic - no remote image, drawn with currentColor. */\nfunction ThumbArt({ index }: { index: number }) {\n  const variant = index % 3\n  return (\n    <svg viewBox=\"0 0 48 48\" className=\"size-full\" aria-hidden=\"true\" focusable=\"false\">\n      {variant === 0 ? (\n        <>\n          <circle cx=\"34\" cy=\"14\" r=\"6\" fill=\"currentColor\" opacity=\"0.55\" />\n          <path d=\"M4 40 L18 20 L30 40 Z\" fill=\"currentColor\" opacity=\"0.85\" />\n          <path d=\"M24 40 L34 26 L44 40 Z\" fill=\"currentColor\" opacity=\"0.5\" />\n        </>\n      ) : null}\n      {variant === 1 ? (\n        <>\n          <rect x=\"6\" y=\"26\" width=\"8\" height=\"16\" rx=\"2\" fill=\"currentColor\" opacity=\"0.5\" />\n          <rect x=\"18\" y=\"16\" width=\"8\" height=\"26\" rx=\"2\" fill=\"currentColor\" opacity=\"0.75\" />\n          <rect x=\"30\" y=\"8\" width=\"8\" height=\"34\" rx=\"2\" fill=\"currentColor\" opacity=\"0.9\" />\n        </>\n      ) : null}\n      {variant === 2 ? (\n        <>\n          <circle cx=\"24\" cy=\"24\" r=\"14\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"3\" opacity=\"0.5\" />\n          <circle cx=\"24\" cy=\"24\" r=\"6\" fill=\"currentColor\" opacity=\"0.9\" />\n        </>\n      ) : null}\n    </svg>\n  )\n}\n\n/* Swatch: round semantic colour swatches; the selected one gets a ring plus a check. */\nexport function SwatchRadio({ className, size = \"md\", options, value, defaultValue, onValueChange }: RadioGroupProps) {\n  const { opts, selected, select, spring } = useRadioGroup({ options, value, defaultValue, onValueChange })\n  const { setRef, onKeyDown } = useRadioKeys(opts, select)\n  const activeIndex = opts.findIndex((o) => o.value === selected)\n\n  return (\n    <div\n      data-slot=\"styled-radio-group\"\n      role=\"radiogroup\"\n      aria-label=\"Choose a color\"\n      className={cn(\"flex flex-wrap items-center gap-2.5\", className)}\n    >\n      {opts.map((opt, i) => {\n        const active = opt.value === selected\n        const roving = active || (activeIndex < 0 && i === 0)\n        const tone = i % TONE_SURFACE.length\n        return (\n          <button\n            key={opt.value}\n            ref={setRef(i)}\n            type=\"button\"\n            role=\"radio\"\n            aria-checked={active}\n            aria-label={typeof opt.label === \"string\" ? opt.label : TONE_NAME[tone]}\n            tabIndex={roving ? 0 : -1}\n            onKeyDown={onKeyDown(i)}\n            onClick={() => select(opt.value)}\n            data-slot=\"styled-radio-item\"\n            data-tone={TONE_NAME[tone]}\n            className={cn(\n              \"relative flex cursor-pointer items-center justify-center rounded-full ring-offset-2 ring-offset-background transition-all\",\n              focusRing,\n              swatchSize[size],\n              TONE_SURFACE[tone],\n              TONE_ON[tone],\n              active ? \"ring-2 ring-foreground\" : \"ring-1 ring-[color-mix(in_oklab,var(--color-foreground)_15%,transparent)] hover:ring-[color-mix(in_oklab,var(--color-foreground)_45%,transparent)]\"\n            )}\n          >\n            <CheckMark active={active} spring={spring} className={checkSize[size]} />\n          </button>\n        )\n      })}\n    </div>\n  )\n}\n\n/* Thumb: small tiles with an inline SVG preview plus a label underneath. */\nexport function ThumbRadio({ className, size = \"md\", options, value, defaultValue, onValueChange }: RadioGroupProps) {\n  const { opts, selected, select, spring } = useRadioGroup({ options, value, defaultValue, onValueChange })\n  const { setRef, onKeyDown } = useRadioKeys(opts, select)\n  const activeIndex = opts.findIndex((o) => o.value === selected)\n\n  return (\n    <div\n      data-slot=\"styled-radio-group\"\n      role=\"radiogroup\"\n      aria-label=\"Choose an option\"\n      className={cn(\"flex flex-wrap items-start gap-3\", 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              \"flex cursor-pointer flex-col items-center gap-1.5 rounded-lg font-medium transition-colors\",\n              focusRing,\n              textSize[size],\n              active ? \"text-foreground\" : \"text-muted-foreground hover:text-foreground\"\n            )}\n          >\n            <span\n              className={cn(\n                \"relative overflow-hidden rounded-lg border bg-surface-2 p-2 transition-colors\",\n                tileSize[size],\n                active ? \"border-primary text-primary\" : \"border-field-border text-muted-foreground\"\n              )}\n            >\n              <ThumbArt index={i} />\n              <CheckMark\n                active={active}\n                spring={spring}\n                className={cn(\n                  \"absolute right-1 top-1 rounded-full bg-primary p-0.5 text-primary-foreground\",\n                  checkSize[size]\n                )}\n              />\n            </span>\n            <span>{opt.label}</span>\n          </button>\n        )\n      })}\n    </div>\n  )\n}\n\n/* Color: labelled colour chips with a semantic tone fill on the left. */\nexport function ColorRadio({ className, size = \"md\", options, value, defaultValue, onValueChange }: RadioGroupProps) {\n  const { opts, selected, select, spring } = useRadioGroup({ options, value, defaultValue, onValueChange })\n  const { setRef, onKeyDown } = useRadioKeys(opts, select)\n  const activeIndex = opts.findIndex((o) => o.value === selected)\n\n  return (\n    <div\n      data-slot=\"styled-radio-group\"\n      role=\"radiogroup\"\n      aria-label=\"Choose a color\"\n      className={cn(\"flex flex-wrap items-center gap-2\", className)}\n    >\n      {opts.map((opt, i) => {\n        const active = opt.value === selected\n        const roving = active || (activeIndex < 0 && i === 0)\n        const tone = i % TONE_SURFACE.length\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            data-tone={TONE_NAME[tone]}\n            className={cn(\n              \"flex cursor-pointer items-center gap-2 rounded-full border py-1 pl-1 pr-3 font-medium transition-colors\",\n              focusRing,\n              textSize[size],\n              active\n                ? \"border-foreground text-foreground\"\n                : \"border-field-border text-muted-foreground hover:border-[color-mix(in_oklab,var(--color-foreground)_35%,transparent)] hover:text-foreground\"\n            )}\n          >\n            <span\n              className={cn(\n                \"flex items-center justify-center rounded-full\",\n                swatchSize[size],\n                TONE_SURFACE[tone],\n                TONE_ON[tone]\n              )}\n            >\n              <CheckMark active={active} spring={spring} className={checkSize[size]} />\n            </span>\n            <span>{opt.label}</span>\n          </button>\n        )\n      })}\n    </div>\n  )\n}\n\n/* Avatar: bas harfli yuvarlak avatarlar; secili olan halka + rozet onay alir. */\nexport function AvatarRadio({ className, size = \"md\", options, value, defaultValue, onValueChange }: RadioGroupProps) {\n  const { opts, selected, select, spring } = useRadioGroup({ options, value, defaultValue, onValueChange })\n  const { setRef, onKeyDown } = useRadioKeys(opts, select)\n  const activeIndex = opts.findIndex((o) => o.value === selected)\n\n  return (\n    <div\n      data-slot=\"styled-radio-group\"\n      role=\"radiogroup\"\n      aria-label=\"Choose a person\"\n      className={cn(\"flex flex-wrap items-center gap-3\", className)}\n    >\n      {opts.map((opt, i) => {\n        const active = opt.value === selected\n        const roving = active || (activeIndex < 0 && i === 0)\n        const tone = i % TONE_SURFACE.length\n        return (\n          <button\n            key={opt.value}\n            ref={setRef(i)}\n            type=\"button\"\n            role=\"radio\"\n            aria-checked={active}\n            aria-label={typeof opt.label === \"string\" ? opt.label : undefined}\n            tabIndex={roving ? 0 : -1}\n            onKeyDown={onKeyDown(i)}\n            onClick={() => select(opt.value)}\n            data-slot=\"styled-radio-item\"\n            data-tone={TONE_NAME[tone]}\n            className={cn(\"relative cursor-pointer rounded-full transition-transform\", focusRing)}\n          >\n            <span\n              className={cn(\n                \"flex items-center justify-center rounded-full font-semibold uppercase ring-offset-2 ring-offset-background transition-all\",\n                swatchSize[size],\n                textSize[size],\n                TONE_SURFACE[tone],\n                TONE_ON[tone],\n                active ? \"ring-2 ring-foreground\" : \"ring-1 ring-[color-mix(in_oklab,var(--color-foreground)_15%,transparent)]\"\n              )}\n            >\n              {initial(opt.label, i)}\n            </span>\n            <CheckMark\n              active={active}\n              spring={spring}\n              className=\"absolute -bottom-0.5 -right-0.5 size-4 rounded-full border border-background bg-primary p-0.5 text-primary-foreground\"\n            />\n          </button>\n        )\n      })}\n    </div>\n  )\n}\n\n/* Tile: buyuk gradyan token karolari; secili olan kenarlik + kose onayi alir. */\nexport function TileRadio({ className, size = \"md\", options, value, defaultValue, onValueChange }: RadioGroupProps) {\n  const { opts, selected, select, spring } = useRadioGroup({ options, value, defaultValue, onValueChange })\n  const { setRef, onKeyDown } = useRadioKeys(opts, select)\n  const activeIndex = opts.findIndex((o) => o.value === selected)\n\n  return (\n    <div\n      data-slot=\"styled-radio-group\"\n      role=\"radiogroup\"\n      aria-label=\"Choose an option\"\n      className={cn(\"flex flex-wrap items-stretch gap-3\", className)}\n    >\n      {opts.map((opt, i) => {\n        const active = opt.value === selected\n        const roving = active || (activeIndex < 0 && i === 0)\n        const tone = i % TONE_SURFACE.length\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            data-tone={TONE_NAME[tone]}\n            className={cn(\n              \"relative flex cursor-pointer flex-col overflow-hidden rounded-xl border text-left font-medium transition-colors\",\n              focusRing,\n              textSize[size],\n              active\n                ? \"border-primary bg-card text-foreground\"\n                : \"border-field-border bg-card text-muted-foreground hover:text-foreground\"\n            )}\n          >\n            <span\n              className={cn(\"flex w-full items-end p-2\", tileMedia[size], TONE_SURFACE[tone])}\n              aria-hidden=\"true\"\n            >\n              <span className={cn(\"block h-1.5 w-8 rounded-full bg-[color-mix(in_oklab,var(--color-background)_70%,transparent)]\")} />\n            </span>\n            <span className=\"px-3 py-2\">{opt.label}</span>\n            <CheckMark\n              active={active}\n              spring={spring}\n              className=\"absolute right-1.5 top-1.5 size-4 rounded-full bg-primary p-0.5 text-primary-foreground\"\n            />\n          </button>\n        )\n      })}\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ui/radio-group-image.tsx"
    }
  ],
  "categories": [
    "styled",
    "radio"
  ],
  "type": "registry:component"
}