{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "JellyRadio-JS-CSS",
	"title": "JellyRadio",
	"description": "Radio group of labelled chips where the chosen one swells wide-then-tall on two springs and barges its neighbours outward with a travelling stagger, so a selection reads as a force moving through the row.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "JellyRadio.css",
			"target": "@components/JellyRadio.css",
			"content": ".jelly-radio {\n  --jr-chip: #27272a;\n  --jr-active: #f5f5f5;\n  --jr-text: #f5f5f5;\n  --jr-active-text: #18181b;\n  --jr-gap: 8px;\n  --jr-radius: 18px;\n  --jr-h: 36px;\n  --jr-font: 13px;\n  --jr-px: 16px;\n  --jr-pad-x: 16px;\n  --jr-pad-y: 6px;\n  --jr-ease-out: cubic-bezier(0.23, 1, 0.32, 1);\n\n  display: inline-flex;\n  align-items: center;\n  gap: var(--jr-gap);\n  padding: var(--jr-pad-y) var(--jr-pad-x);\n  user-select: none;\n  -webkit-user-select: none;\n  -webkit-touch-callout: none;\n}\n\n.jelly-radio[data-disabled] {\n  opacity: 0.5;\n  pointer-events: none;\n}\n\n.jelly-radio__chip {\n  position: relative;\n  margin: 0;\n  padding: 0;\n  border: 0;\n  background: none;\n  color: inherit;\n  font: inherit;\n  cursor: pointer;\n  outline: none;\n  transform-origin: 50% 50%;\n  touch-action: manipulation;\n  -webkit-tap-highlight-color: transparent;\n}\n\n.jelly-radio__chip[data-on='true'] {\n  cursor: default;\n}\n\n.jelly-radio:not([data-disabled]) .jelly-radio__chip:disabled {\n  cursor: default;\n  opacity: 0.4;\n}\n\n.jelly-radio__skin {\n  position: relative;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  gap: 0.4em;\n  height: var(--jr-h);\n  padding: 0 var(--jr-px);\n  border-radius: var(--jr-radius);\n  overflow: hidden;\n  background: var(--jr-chip);\n  color: var(--jr-text);\n  font-size: var(--jr-font);\n  font-weight: 500;\n  line-height: 1;\n  white-space: nowrap;\n  transition:\n    transform 160ms var(--jr-ease-out),\n    background-color 200ms ease,\n    color 200ms ease;\n}\n\n.jelly-radio__skin::before {\n  content: '';\n  position: absolute;\n  inset: 0;\n  background: var(--jr-text);\n  opacity: 0;\n  pointer-events: none;\n  transition: opacity 160ms ease;\n}\n\n@media (hover: hover) and (pointer: fine) {\n  .jelly-radio__chip[data-on='false']:not(:disabled):hover .jelly-radio__skin::before {\n    opacity: 0.11;\n  }\n}\n\n.jelly-radio__chip:active .jelly-radio__skin {\n  transform: scale(0.97);\n}\n\n.jelly-radio__chip[data-on='true'] .jelly-radio__skin {\n  background: var(--jr-active);\n  color: var(--jr-active-text);\n}\n\n.jelly-radio__icon {\n  display: inline-flex;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .jelly-radio__chip:active .jelly-radio__skin {\n    transform: none;\n  }\n}\n"
		},
		{
			"type": "registry:component",
			"path": "JellyRadio.jsx",
			"content": "'use client';\n\nimport { forwardRef, useEffect, useLayoutEffect, useRef, useState } from 'react';\nimport { animate, motion, motionValue, useReducedMotion, useTransform } from 'motion/react';\n\nimport './JellyRadio.css';\n\nconst DEFAULT_ITEMS = ['Off', 'Low', 'Medium', 'High', 'Max'];\nconst SIZES = { sm: [28, 12, 12], md: [36, 13, 16], lg: [44, 14, 20] };\n\nconst spring = (k, m, bounce) => ({\n  type: 'spring',\n  stiffness: k,\n  damping: 2 * Math.sqrt(k * m) * (1 - bounce),\n  mass: m\n});\n\nconst Chip = forwardRef(function Chip({ mv, children, ...rest }, ref) {\n  const transform = useTransform(() => `translateX(${mv.x.get()}px) scale(${mv.sx.get()}, ${mv.sy.get()})`);\n  return (\n    <motion.button ref={ref} style={{ transform }} {...rest}>\n      {children}\n    </motion.button>\n  );\n});\n\nexport default function JellyRadio({\n  items = DEFAULT_ITEMS,\n  value,\n  defaultValue,\n  onChange,\n  chipColor = '#27272a',\n  activeColor = '#f5f5f5',\n  textColor = '#f5f5f5',\n  activeTextColor = '#18181b',\n  size = 'md',\n  gap = 8,\n  radius = 18,\n  swell = 0.2,\n  barge = 6,\n  shrink = 0.05,\n  jelly = 1,\n  bounce = 0.25,\n  stagger = 22,\n  stiffness = 580,\n  disabled = false,\n  ariaLabel = 'Options',\n  className = ''\n}) {\n  const list = items.map(it => (typeof it === 'string' ? { value: it, label: it } : it));\n  const [inner, setInner] = useState(() => defaultValue ?? list[0]?.value);\n  const current = value ?? inner;\n  const at = Math.max(\n    0,\n    list.findIndex(it => it.value === current)\n  );\n  const reduce = useReducedMotion();\n  const groupRef = useRef(null);\n  const chipRefs = useRef([]);\n  const widths = useRef([]);\n  const mvs = useRef([]);\n  const applied = useRef(at);\n  const cfg = useRef({});\n  cfg.current = { swell, barge, shrink, jelly, bounce, stagger, stiffness, reduce, count: list.length };\n  const [h, font, px] = SIZES[size] ?? SIZES.md;\n  const itemsKey = list.map(it => it.value).join('|');\n\n  const mvFor = i => {\n    let mv = mvs.current[i];\n    if (!mv) {\n      mv = { x: motionValue(0), sx: motionValue(1), sy: motionValue(1) };\n      mvs.current[i] = mv;\n    }\n    return mv;\n  };\n\n  const apply = (sel, instant) => {\n    const C = cfg.current;\n    const group = groupRef.current;\n    const rtl = group ? getComputedStyle(group).direction === 'rtl' : false;\n    const push = ((widths.current[sel] ?? 0) * C.swell) / 2 + C.barge;\n    for (let i = 0; i < C.count; i++) {\n      const mv = mvFor(i);\n      const on = i === sel;\n      const far = Math.abs(i - sel);\n      const dir = Math.sign(i - sel) * (rtl ? -1 : 1);\n      const x = dir * push;\n      const s = on ? 1 + C.swell : 1 - C.shrink;\n      if (instant || C.reduce) {\n        mv.x.jump(x);\n        mv.sx.jump(s);\n        mv.sy.jump(s);\n        continue;\n      }\n      const k = C.stiffness * (1 - 0.12 * Math.min(far, 3));\n      const inFlight = mv.x.isAnimating() || mv.sx.isAnimating() || mv.sy.isAnimating();\n      const delay = inFlight ? 0 : (far * C.stagger) / 1000;\n      animate(mv.x, x, { ...spring(k, 0.9, C.bounce), delay });\n      const j = C.jelly;\n      animate(mv.sx, s, {\n        ...spring(k * (1 + 0.24 * j), 0.9 - 0.1 * j, Math.min(0.85, C.bounce + 0.3 * j)),\n        delay\n      });\n      animate(mv.sy, s, { ...spring(k * (1 - 0.14 * j), 0.9 + 0.05 * j, C.bounce), delay: delay + 0.05 * j });\n    }\n  };\n\n  const measure = () => {\n    const group = groupRef.current;\n    if (!group) return;\n    widths.current = chipRefs.current.map(el => el?.offsetWidth ?? 0);\n    const chipH = chipRefs.current[0]?.offsetHeight ?? 0;\n    const maxW = Math.max(0, ...widths.current);\n    group.style.setProperty('--jr-pad-x', `${Math.ceil((maxW * swell * 1.3) / 2 + barge) + 2}px`);\n    group.style.setProperty('--jr-pad-y', `${Math.ceil((chipH * swell) / 2) + 2}px`);\n  };\n  useLayoutEffect(() => {\n    const settle = () => {\n      measure();\n      apply(applied.current, true);\n    };\n    settle();\n    const observer = new ResizeObserver(settle);\n    if (groupRef.current) observer.observe(groupRef.current);\n    document.fonts?.ready.then(settle);\n    return () => observer.disconnect();\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [itemsKey, size, gap, swell, barge, shrink]);\n  useEffect(() => {\n    if (applied.current === at) return;\n    applied.current = at;\n    apply(at, true);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [at]);\n  useEffect(\n    () => () =>\n      mvs.current.forEach(mv => {\n        mv.x.destroy();\n        mv.sx.destroy();\n        mv.sy.destroy();\n      }),\n    []\n  );\n\n  const commit = (i, instant) => {\n    if (disabled || i === at || !list[i] || list[i].disabled) return;\n    applied.current = i;\n    apply(i, instant);\n    if (value === undefined) setInner(list[i].value);\n    onChange?.(list[i].value, i);\n  };\n  const stepFrom = (i, dir) => {\n    const n = list.length;\n    let j = i;\n    for (let tries = 0; tries < n; tries++) {\n      j = (j + dir + n) % n;\n      if (!list[j].disabled) return j;\n    }\n    return i;\n  };\n  const onKeyDown = (e, i) => {\n    let next = null;\n    if (e.key === 'ArrowRight' || e.key === 'ArrowDown') next = stepFrom(i, 1);\n    else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') next = stepFrom(i, -1);\n    else if (e.key === 'Home') next = stepFrom(-1, 1);\n    else if (e.key === 'End') next = stepFrom(list.length, -1);\n    else if (e.key === ' ' || e.key === 'Enter') next = i;\n    if (next === null) return;\n    e.preventDefault();\n    commit(next, true);\n    chipRefs.current[next]?.focus();\n  };\n\n  return (\n    <div\n      ref={groupRef}\n      role=\"radiogroup\"\n      aria-label={ariaLabel}\n      data-disabled={disabled ? '' : undefined}\n      className={`jelly-radio${className ? ` ${className}` : ''}`}\n      style={{\n        '--jr-chip': chipColor,\n        '--jr-active': activeColor,\n        '--jr-text': textColor,\n        '--jr-active-text': activeTextColor,\n        '--jr-gap': `${gap}px`,\n        '--jr-radius': `${radius}px`,\n        '--jr-h': `${h}px`,\n        '--jr-font': `${font}px`,\n        '--jr-px': `${px}px`\n      }}\n    >\n      {list.map((it, i) => (\n        <Chip\n          key={it.value}\n          mv={mvFor(i)}\n          ref={el => {\n            chipRefs.current[i] = el;\n          }}\n          type=\"button\"\n          role=\"radio\"\n          aria-checked={i === at}\n          tabIndex={i === at ? 0 : -1}\n          disabled={disabled || !!it.disabled}\n          className=\"jelly-radio__chip\"\n          data-on={i === at ? 'true' : 'false'}\n          onClick={e => commit(i, e.detail === 0)}\n          onKeyDown={e => onKeyDown(e, i)}\n        >\n          <span className=\"jelly-radio__skin\">\n            {it.icon ? <span className=\"jelly-radio__icon\">{it.icon}</span> : null}\n            <span className=\"jelly-radio__label\">{it.label}</span>\n          </span>\n        </Chip>\n      ))}\n    </div>\n  );\n}\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"motion@^12.23.12"
	]
}