{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "switcher",
  "type": "registry:ui",
  "title": "Switcher",
  "description": "An accessible React segmented control for Next.js: one choice from a few options, with a pill that slides to the one you pick.",
  "author": "FewTone <https://fewtone.com>",
  "dependencies": [
    "class-variance-authority@^0.7.1",
    "motion@^14.0.0",
    "radix-ui@^1.7.0"
  ],
  "registryDependencies": [
    "https://ui.fewtone.com/r/tokens.json",
    "https://ui.fewtone.com/r/motion.json"
  ],
  "files": [
    {
      "path": "registry/fewtone/ui/switcher.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { spring } from \"@/lib/motion\";\nimport { cn } from \"@/lib/utils\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { LayoutGroup, motion, useReducedMotion } from \"motion/react\";\nimport { ToggleGroup as ToggleGroupPrimitive } from \"radix-ui\";\nimport {\n  type ComponentProps,\n  createContext,\n  type Ref,\n  useContext,\n  useId,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\n\n/*\n * Switcher: one choice from a few, like Day / Week / Month (a segmented control). Owner, 2026-10-14: split\n * from Toggle Group, which switches several options on and off; Lab only until it's approved, so no meta\n * yet.\n * - A pill slides to the active option (layoutId on spring.snappy, .agents/rules/motion.md), scoped to each\n *   Switcher, so two never trade a pill.\n * - There's always a choice: clicking the active option keeps it.\n * - A radiogroup of radio items (Radix Toggle Group, single). Arrow keys move between options; Home and End\n *   jump to the ends.\n * - Flat. The pill takes the page's own colour, dark in dark mode and light in light mode (owner,\n *   2026-10-14). Default: a muted track. Outline: a bordered track, and the pill gets a hairline too, so it\n *   shows on any surface. No shadows and no\n *   scale. Inactive options are muted and brighten on hover.\n * - The pill's corners follow the option's own radius (read from it), so they stay true while it changes\n *   width between options of different sizes.\n */\n\nconst switcherVariants = cva(\"relative isolate inline-flex w-fit items-center gap-0.5 rounded-lg p-0.5\", {\n  variants: {\n    variant: {\n      default: \"bg-muted\",\n      outline: \"border border-input\",\n    },\n  },\n  defaultVariants: { variant: \"default\" },\n});\n\n// The options sit 2px inside the track, so the track is a control height (32, 36 or 40px)\nconst switcherItemVariants = cva(\n  [\n    \"relative inline-flex shrink-0 cursor-pointer items-center justify-center gap-1.5 rounded-md text-sm font-medium whitespace-nowrap\",\n    \"text-muted-foreground transition-colors duration-(--duration-base) ease-out data-[state=off]:hover:text-foreground data-[state=on]:text-foreground\",\n    \"outline-ring focus-visible:outline-(length:--focus-ring-width) focus-visible:outline-offset-(--focus-ring-offset)\",\n    \"disabled:pointer-events-none disabled:opacity-(--opacity-disabled)\",\n    \"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-(--size-icon-md)\",\n  ],\n  {\n    variants: {\n      size: {\n        sm: \"h-7 px-2.5\",\n        md: \"h-8 px-3\",\n        lg: \"h-9 px-4\",\n      },\n    },\n    defaultVariants: { size: \"md\" },\n  },\n);\n\nconst SwitcherContext = createContext<{\n  value: string;\n  variant: \"default\" | \"outline\";\n  size: \"sm\" | \"md\" | \"lg\";\n  reduce: boolean;\n} | null>(null);\n\ntype RootProps = Omit<\n  Extract<ComponentProps<typeof ToggleGroupPrimitive.Root>, { type: \"single\" }>,\n  \"type\" | \"value\" | \"defaultValue\" | \"onValueChange\"\n>;\n\nexport type SwitcherProps = RootProps &\n  VariantProps<typeof switcherVariants> &\n  VariantProps<typeof switcherItemVariants> & {\n    /** The chosen option, when you control it */\n    value?: string;\n    /** The option chosen at first */\n    defaultValue?: string;\n    /** Called with the new option when the choice changes */\n    onValueChange?: (value: string) => void;\n  };\n\nfunction Switcher({\n  value,\n  defaultValue,\n  onValueChange,\n  variant,\n  size,\n  className,\n  children,\n  ...props\n}: SwitcherProps) {\n  const groupId = useId();\n  const reduce = useReducedMotion() ?? false;\n  // Mirror the value, controlled or not, so options know whether they're on\n  const [internal, setInternal] = useState(defaultValue ?? \"\");\n  const current = value ?? internal;\n\n  return (\n    <SwitcherContext.Provider\n      value={{ value: current, variant: variant ?? \"default\", size: size ?? \"md\", reduce }}\n    >\n      <ToggleGroupPrimitive.Root\n        data-slot=\"switcher\"\n        data-variant={variant ?? \"default\"}\n        type=\"single\"\n        value={current}\n        onValueChange={(next) => {\n          // There's always a choice: Radix sends \"\" when the active option is clicked again\n          if (!next) return;\n          setInternal(next);\n          onValueChange?.(next);\n        }}\n        className={cn(switcherVariants({ variant }), className)}\n        {...props}\n      >\n        <LayoutGroup id={groupId}>{children}</LayoutGroup>\n      </ToggleGroupPrimitive.Root>\n    </SwitcherContext.Provider>\n  );\n}\n\n/** Hands the option's node to our ref and to the caller's */\nfunction useMergedRef<T>(own: { current: T | null }, theirs?: Ref<T>) {\n  return (node: T | null) => {\n    own.current = node;\n    if (typeof theirs === \"function\") theirs(node);\n    else if (theirs) (theirs as { current: T | null }).current = node;\n  };\n}\n\nfunction SwitcherItem({\n  className,\n  children,\n  value,\n  ref,\n  ...props\n}: ComponentProps<typeof ToggleGroupPrimitive.Item>) {\n  const switcher = useContext(SwitcherContext);\n  const node = useRef<HTMLButtonElement>(null);\n  const setRef = useMergedRef(node, ref);\n  const [radius, setRadius] = useState(0);\n\n  // A numeric radius lets motion keep the pill's corners true while it changes width\n  useLayoutEffect(() => {\n    if (node.current) setRadius(Number.parseFloat(getComputedStyle(node.current).borderTopLeftRadius) || 0);\n  }, []);\n\n  return (\n    <ToggleGroupPrimitive.Item\n      ref={setRef}\n      data-slot=\"switcher-item\"\n      value={value}\n      className={cn(switcherItemVariants({ size: switcher?.size }), className)}\n      {...props}\n    >\n      {switcher?.value === value ? (\n        // Options aren't stacking contexts: the pill joins the Switcher's layer (it's `isolate`), so it passes\n        // behind every option's label as it slides\n        <motion.span\n          layoutId=\"switcher-pill\"\n          aria-hidden\n          className={cn(\n            \"absolute inset-0 -z-10\",\n            switcher.variant === \"outline\" ? \"border border-input bg-background\" : \"bg-background\",\n          )}\n          style={{ borderRadius: radius }}\n          transition={switcher.reduce ? { duration: 0 } : spring.snappy}\n        />\n      ) : null}\n      {children}\n    </ToggleGroupPrimitive.Item>\n  );\n}\n\nexport { Switcher, SwitcherItem, switcherItemVariants, switcherVariants };\n"
    }
  ],
  "categories": [
    "buttons"
  ],
  "docs": "Usage and examples: https://ui.fewtone.com/components/switcher"
}
