{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "toggle-group-motion",
  "title": "Group-motion toggle",
  "description": "Styled toggle: the Group-motion set. 5 decorative toggle variations (SlideToggleGroup, PopToggleGroup, FadeToggleGroup, SpringToggleGroup, MorphToggleGroup) 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/toggle-group-motion.tsx",
      "content": "\"use client\"\r\n\r\nimport * as React from \"react\"\r\nimport { AlignCenter, AlignLeft, AlignRight } from \"lucide-react\"\r\nimport { motion, useReducedMotion } from \"motion/react\"\r\n\r\nimport { cn } from \"@/lib/utils\"\r\n\r\n/* Motion toggle group family: 5 decorative single-select controls. The shared idea\n   is carrying the ACTIVE INDICATOR between options with animation: because the\n   indicator is shared through a framer `layoutId` it slides to the new option rather\n   than jumping. The variants only change the transition character of the indicator\n   (a soft slide, pop, dissolve, a springy hop, a morphing bar). Every button is a\n   real <button> + aria-pressed + a focus-visible ring. Color comes ONLY from\n   semantic tokens; alpha via color-mix. While useReducedMotion is on the indicator\n   moves instantly. The layoutId is unique per instance via React.useId(), so groups\n   on the same page do not leak animation into each other. */\r\n\r\nexport type StyledSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\r\n\r\ntype Option = { value: string; label?: React.ReactNode; icon?: React.ReactNode }\r\n\r\ntype ToggleGroupProps = {\r\n  className?: string\r\n  size?: StyledSize\r\n  type?: \"single\" | \"multiple\"\r\n  options?: Option[]\r\n  value?: string | string[]\r\n  defaultValue?: string | string[]\r\n  onValueChange?: (v: string | string[]) => void\r\n}\r\n\r\nconst sizeBtn: Record<StyledSize, string> = {\r\n  sm: \"h-8 px-3 text-xs\",\r\n  md: \"h-9 px-4 text-sm\",\r\n  lg: \"h-10 px-5 text-sm\",\r\n  xl: \"h-11 px-6 text-base\",\r\n}\r\n\r\nconst btnBase =\r\n  \"relative z-10 inline-flex select-none items-center justify-center gap-2 whitespace-nowrap font-medium outline-none transition-colors focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0 [&_i]:text-base [&_i]:leading-none\"\r\n\r\nconst TEXT_OPTIONS: Option[] = [\r\n  { value: \"left\", label: \"Left\", icon: <AlignLeft /> },\r\n  { value: \"center\", label: \"Center\", icon: <AlignCenter /> },\r\n  { value: \"right\", label: \"Right\", icon: <AlignRight /> },\r\n]\r\n\r\nfunction toArray(v: string | string[] | undefined): string[] {\r\n  if (v === undefined) return []\r\n  return Array.isArray(v) ? v : [v]\r\n}\r\n\r\n/* Kontrollu/kontrolsuz secim durumu. Tek-secimde varsayilan ilk secenektir. */\r\nfunction useToggleGroup(props: ToggleGroupProps, fallback: Option[]) {\r\n  const { type = \"single\", options, value, defaultValue, onValueChange } = props\r\n  const list = React.useMemo(() => (options && options.length ? options : fallback), [options, fallback])\r\n  const controlled = value !== undefined\r\n  const [internal, setInternal] = React.useState<string[]>(() => {\r\n    const init = toArray(defaultValue)\r\n    if (init.length === 0 && type === \"single\" && defaultValue === undefined) {\r\n      return list[0] ? [list[0].value] : []\r\n    }\r\n    return init\r\n  })\r\n  const selected = controlled ? toArray(value) : internal\r\n\r\n  const emit = React.useCallback(\r\n    (next: string[]) => {\r\n      if (!controlled) setInternal(next)\r\n      onValueChange?.(type === \"single\" ? (next[0] ?? \"\") : next)\r\n    },\r\n    [controlled, onValueChange, type]\r\n  )\r\n\r\n  const toggle = React.useCallback(\r\n    (v: string) => {\r\n      const on = selected.includes(v)\r\n      if (type === \"single\") {\r\n        emit(on ? [] : [v])\r\n      } else {\r\n        emit(on ? selected.filter((s) => s !== v) : [...selected, v])\r\n      }\r\n    },\r\n    [selected, type, emit]\r\n  )\r\n\r\n  const isOn = React.useCallback((v: string) => selected.includes(v), [selected])\r\n  return { list, isOn, toggle }\r\n}\r\n\r\ntype Spec = {\r\n  /** Root shell class. */\r\n  root: string\r\n  /** Button class. */\r\n  item: string\r\n  /** Aktif dugme metin rengi. */\r\n  onText: string\r\n  /** Kapali dugme metin rengi. */\r\n  offText: string\r\n  /** Indicator class. */\r\n  indicator: string\r\n  /** The indicator's enter/exit animation (skipped under reduced motion). */\r\n  pop?: boolean\r\n  /** Gosterge gecis ayari. */\r\n  transition: Record<string, unknown>\r\n  /** Erisilebilir ad. */\r\n  label: string\r\n}\r\n\r\n/* The shared shell: only the indicator transition and the surface change. */\r\nfunction MotionGroup({ props, spec }: { props: ToggleGroupProps; spec: Spec }) {\r\n  const { className, size = \"md\" } = props\r\n  const { list, isOn, toggle } = useToggleGroup({ ...props, type: \"single\" }, TEXT_OPTIONS)\r\n  const id = React.useId()\r\n  const reduce = useReducedMotion()\r\n  const transition = reduce ? { duration: 0 } : spec.transition\r\n\r\n  return (\r\n    <div\r\n      data-slot=\"styled-toggle-group-viewport\"\r\n      className=\"-m-1 flex max-w-full overflow-x-auto p-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\"\r\n    >\r\n      <div\r\n        data-slot=\"styled-toggle-group\"\r\n        role=\"group\"\r\n        aria-label={spec.label}\r\n        className={cn(spec.root, className)}\r\n      >\r\n        {list.map((o) => {\r\n          const on = isOn(o.value)\r\n          return (\r\n            <button\r\n              key={o.value}\r\n              type=\"button\"\r\n              aria-pressed={on}\r\n              data-slot=\"styled-toggle-group-item\"\r\n              onClick={() => toggle(o.value)}\r\n              className={cn(btnBase, sizeBtn[size], spec.item, on ? spec.onText : spec.offText)}\r\n            >\r\n              {on && (\r\n                <motion.span\r\n                  layoutId={`${id}-motion-indicator`}\r\n                  transition={transition}\r\n                  initial={spec.pop && !reduce ? { opacity: 0, scale: 0.86 } : false}\r\n                  animate={{ opacity: 1, scale: 1 }}\r\n                  className={cn(\"absolute inset-0 -z-10\", spec.indicator)}\r\n                />\r\n              )}\r\n              {o.icon}\r\n              {o.label}\r\n            </button>\r\n          )\r\n        })}\r\n      </div>\r\n    </div>\r\n  )\r\n}\r\n\r\n/* Slide: gosterge yumusak bir yayla yeni segmente kayar. Klasik segment kontrol. */\r\nexport function SlideToggleGroup(props: ToggleGroupProps) {\r\n  return (\r\n    <MotionGroup\r\n      props={props}\r\n      spec={{\r\n        label: \"Text alignment, sliding indicator\",\r\n        root: \"inline-flex items-center gap-0.5 rounded-lg border border-border bg-muted p-1\",\r\n        item: \"rounded-md\",\r\n        onText: \"text-foreground\",\r\n        offText: \"text-muted-foreground hover:text-foreground\",\r\n        indicator: \"rounded-md border border-border bg-background shadow-sm\",\r\n        transition: { type: \"spring\" as const, stiffness: 420, damping: 34 },\r\n      }}\r\n    />\r\n  )\r\n}\r\n\r\n/* Pop: the indicator grows slightly as it slides and settles into place. */\r\nexport function PopToggleGroup(props: ToggleGroupProps) {\r\n  return (\r\n    <MotionGroup\r\n      props={props}\r\n      spec={{\r\n        label: \"Text alignment, popping indicator\",\r\n        root: \"inline-flex items-center gap-0.5 rounded-lg bg-surface-2 p-1\",\r\n        item: \"rounded-md\",\r\n        onText: \"text-primary-foreground\",\r\n        offText: \"text-muted-foreground hover:text-foreground\",\r\n        indicator: \"rounded-md bg-primary shadow-sm\",\r\n        pop: true,\r\n        transition: { type: \"spring\" as const, stiffness: 520, damping: 26 },\r\n      }}\r\n    />\r\n  )\r\n}\r\n\r\n/* Fade: gosterge kayarken opaklikla cozulur; en sakin gecis. */\r\nexport function FadeToggleGroup(props: ToggleGroupProps) {\r\n  return (\r\n    <MotionGroup\r\n      props={props}\r\n      spec={{\r\n        label: \"Text alignment, fading indicator\",\r\n        root: \"inline-flex items-center gap-0.5 rounded-lg border border-border bg-background p-1\",\r\n        item: \"rounded-md\",\r\n        onText: \"text-primary\",\r\n        offText: \"text-muted-foreground hover:text-foreground\",\r\n        indicator:\r\n          \"rounded-md bg-[color-mix(in_oklab,var(--color-primary)_14%,transparent)] ring-1 ring-[color-mix(in_oklab,var(--color-primary)_30%,transparent)]\",\r\n        pop: true,\r\n        transition: { duration: 0.28, ease: \"easeOut\" },\r\n      }}\r\n    />\r\n  )\r\n}\r\n\r\n/* Spring: yuksek geri tepmeli yay; gosterge hedefin etrafinda salinir. */\r\nexport function SpringToggleGroup(props: ToggleGroupProps) {\r\n  return (\r\n    <MotionGroup\r\n      props={props}\r\n      spec={{\r\n        label: \"Text alignment, springy indicator\",\r\n        root: \"inline-flex items-center gap-0.5 rounded-full border border-border bg-muted p-1\",\r\n        item: \"rounded-full\",\r\n        onText: \"text-foreground\",\r\n        offText: \"text-muted-foreground hover:text-foreground\",\r\n        indicator: \"rounded-full border border-border bg-background shadow-sm\",\r\n        transition: { type: \"spring\" as const, stiffness: 700, damping: 18, mass: 0.7 },\r\n      }}\r\n    />\r\n  )\r\n}\r\n\r\n/* Morph: the indicator is not a filled box but a thin bar sliding under the active\n   option. */\r\nexport function MorphToggleGroup(props: ToggleGroupProps) {\r\n  return (\r\n    <MotionGroup\r\n      props={props}\r\n      spec={{\r\n        label: \"Text alignment, morphing underline\",\r\n        root: \"inline-flex items-center gap-1 border-b border-border\",\r\n        item: \"rounded-none rounded-t-md\",\r\n        onText: \"text-foreground\",\r\n        offText: \"text-muted-foreground hover:text-foreground\",\r\n        indicator:\r\n          \"rounded-t-md bg-[color-mix(in_oklab,var(--color-foreground)_6%,transparent)] after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:rounded-full after:bg-primary\",\r\n        transition: { type: \"spring\" as const, stiffness: 460, damping: 32 },\r\n      }}\r\n    />\r\n  )\r\n}\r\n",
      "type": "registry:component",
      "target": "components/ui/toggle-group-motion.tsx"
    }
  ],
  "categories": [
    "styled",
    "toggle"
  ],
  "type": "registry:component"
}