{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "toggle-group",
  "title": "Toggle Group",
  "author": "AlgorizeTech",
  "description": "A set of toggle buttons behaving as either radio group or checkbox group. Use it for view switches and filter bars where all options should stay visible.",
  "dependencies": [],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/custom/blocks/ToggleGroupBlock.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport React, {\n  createContext,\n  forwardRef,\n  useContext,\n  useEffect,\n  useImperativeHandle,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { getThemeStyles } from \"./toggle-group-theme\";\n\n// ==========================================\n// THEME & CONFIGURATION\n// ==========================================\nexport type ToggleGroupColor =\n  | \"primary\"\n  | \"indigo\"\n  | \"rose\"\n  | \"emerald\"\n  | \"zinc\"\n  | (string & {});\n/**\n * `segmented` is a pill track whose highlight slides to the pressed item —\n * a view switch (Day / Week / Month). In a single group the pressed item\n * stays pressed when clicked again, and the arrow keys move the choice.\n */\nexport type ToggleGroupVariant = \"default\" | \"outline\" | \"ghost\" | \"segmented\";\nexport type ToggleGroupSize = \"sm\" | \"md\" | \"lg\";\nexport type ToggleGroupType = \"single\" | \"multiple\";\n\n// ==========================================\n// CONTEXT\n// ==========================================\ninterface ToggleGroupContextProps {\n  type: ToggleGroupType;\n  value: string | string[];\n  onValueChange: (value: string | string[]) => void;\n  variant: ToggleGroupVariant;\n  color: ToggleGroupColor;\n  size: ToggleGroupSize;\n  disabled: boolean;\n  theme: Record<string, string>;\n  /** Segmented only: the sliding highlight has been placed, so items drop their own. */\n  pillReady: boolean;\n}\n\n// The segmented track. Inside a `soft` table it takes the table's own well\n// (`--tbl-well`), so the tabs and the search field share one tint.\nconst SEGMENTED_TRACK =\n  \"bg-[var(--tbl-well,color-mix(in_oklab,var(--primary)_5%,color-mix(in_oklab,var(--foreground)_5%,var(--card))))]\";\nconst SEGMENTED_PILL = \"bg-card shadow-sm ring-1 ring-foreground/[0.08]\";\n\nconst segmentedHeights: Record<ToggleGroupSize, string> = {\n  sm: \"h-9\",\n  md: \"h-10\",\n  lg: \"h-12\",\n};\n\n// Layout effects warn during server rendering on older React; the pill is\n// only ever measured in the browser anyway.\nconst useIsomorphicLayoutEffect =\n  typeof window !== \"undefined\" ? useLayoutEffect : useEffect;\n\nconst ToggleGroupContext = createContext<ToggleGroupContextProps | null>(null);\n\nfunction useToggleGroup() {\n  const context = useContext(ToggleGroupContext);\n  if (!context) {\n    throw new Error(\"ToggleGroupItem must be used within a ToggleGroup\");\n  }\n  return context;\n}\n\n// ==========================================\n// GROUP COMPONENT\n// ==========================================\n/** Everything both selection modes share. */\ninterface ToggleGroupBaseProps\n  extends Omit<React.HTMLAttributes<HTMLDivElement>, \"defaultValue\"> {\n  variant?: ToggleGroupVariant;\n  color?: ToggleGroupColor;\n  size?: ToggleGroupSize;\n  disabled?: boolean;\n}\n\n/** The default: one item pressed at a time, so the value is a string. */\nexport interface ToggleGroupSingleProps extends ToggleGroupBaseProps {\n  type?: \"single\";\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n}\n\n/** `type=\"multiple\"` — value and callback both deal in arrays. */\nexport interface ToggleGroupMultipleProps extends ToggleGroupBaseProps {\n  type: \"multiple\";\n  value?: string[];\n  defaultValue?: string[];\n  onValueChange?: (value: string[]) => void;\n}\n\n/**\n * Discriminated on `type`, so a single group hands your callback a `string`\n * and a multiple group hands it a `string[]`, with no cast either way.\n */\nexport type ToggleGroupProps = ToggleGroupSingleProps | ToggleGroupMultipleProps;\n\n/** The two modes collapsed, which is what the implementation works with. */\ntype ToggleGroupImplProps = ToggleGroupBaseProps & {\n  type?: ToggleGroupType;\n  value?: string | string[];\n  defaultValue?: string | string[];\n  onValueChange?: (value: string | string[]) => void;\n};\n\nconst ToggleGroupImpl = forwardRef<HTMLDivElement, ToggleGroupImplProps>(\n  (\n    {\n      type = \"single\",\n      value: controlledValue,\n      defaultValue,\n      onValueChange,\n      variant = \"default\",\n      color = \"primary\",\n      size = \"md\",\n      disabled = false,\n      className,\n      children,\n      onKeyDown,\n      style: styleProp,\n      ...props\n    },\n    ref,\n  ) => {\n    const { theme, style } = getThemeStyles(color as string);\n    const [uncontrolledValue, setUncontrolledValue] = React.useState<\n      string | string[]\n    >(defaultValue !== undefined ? defaultValue : type === \"single\" ? \"\" : []);\n\n    const isControlled = controlledValue !== undefined;\n    const currentValue = isControlled ? controlledValue : uncontrolledValue;\n\n    const handleValueChange = (newValue: string | string[]) => {\n      if (!isControlled) setUncontrolledValue(newValue);\n      if (onValueChange) onValueChange(newValue);\n    };\n\n    const groupRef = useRef<HTMLDivElement>(null);\n    useImperativeHandle(ref, () => groupRef.current as HTMLDivElement);\n\n    // One highlight that slides between items, for a single segmented group.\n    // It is measured from the pressed item, so labels of any width work.\n    const sliding = variant === \"segmented\" && type === \"single\";\n    const pillRef = useRef<HTMLSpanElement>(null);\n    const placedRef = useRef(false);\n    const [pillReady, setPillReady] = useState(false);\n    const valueKey = Array.isArray(currentValue)\n      ? currentValue.join(\"|\")\n      : currentValue;\n\n    useIsomorphicLayoutEffect(() => {\n      const group = groupRef.current;\n      const pill = pillRef.current;\n      if (!sliding || !group || !pill) return;\n\n      const place = () => {\n        const active = group.querySelector<HTMLElement>(\n          '[data-toggle-item][data-state=\"on\"]',\n        );\n        if (!active) {\n          pill.style.opacity = \"0\";\n          return;\n        }\n        const g = group.getBoundingClientRect();\n        const a = active.getBoundingClientRect();\n        // The first placement jumps; every later one slides.\n        if (!placedRef.current) pill.style.transition = \"none\";\n        pill.style.width = `${a.width}px`;\n        pill.style.height = `${a.height}px`;\n        pill.style.transform = `translate(${a.left - g.left - group.clientLeft + group.scrollLeft}px, ${a.top - g.top - group.clientTop}px)`;\n        pill.style.opacity = \"1\";\n        if (!placedRef.current) {\n          void pill.offsetWidth;\n          pill.style.transition = \"\";\n          placedRef.current = true;\n          setPillReady(true);\n        }\n      };\n\n      place();\n      const observer = new ResizeObserver(place);\n      observer.observe(group);\n      group\n        .querySelectorAll<HTMLElement>(\"[data-toggle-item]\")\n        .forEach((item) => observer.observe(item));\n      return () => observer.disconnect();\n    }, [sliding, valueKey]);\n\n    // Segmented single groups behave as a radio group: the arrow keys move\n    // the choice (and focus) to the next enabled item.\n    const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n      onKeyDown?.(event);\n      if (event.defaultPrevented || !sliding || disabled) return;\n      const keys = [\"ArrowRight\", \"ArrowDown\", \"ArrowLeft\", \"ArrowUp\", \"Home\", \"End\"];\n      if (!keys.includes(event.key)) return;\n      const items = Array.from(\n        groupRef.current?.querySelectorAll<HTMLButtonElement>(\n          \"[data-toggle-item]:not(:disabled)\",\n        ) ?? [],\n      );\n      if (!items.length) return;\n      const from = items.findIndex((item) => item === document.activeElement);\n      const last = items.length - 1;\n      const forward = event.key === \"ArrowRight\" || event.key === \"ArrowDown\";\n      const next =\n        event.key === \"Home\"\n          ? 0\n          : event.key === \"End\"\n            ? last\n            : forward\n              ? from >= last ? 0 : from + 1\n              : from <= 0 ? last : from - 1;\n      const target = items[next];\n      if (!target) return;\n      event.preventDefault();\n      target.focus();\n      const nextValue = target.dataset.value;\n      if (nextValue !== undefined) handleValueChange(nextValue);\n    };\n\n    return (\n      <ToggleGroupContext.Provider\n        value={{\n          type,\n          value: currentValue,\n          onValueChange: handleValueChange,\n          variant,\n          color,\n          size,\n          disabled,\n          theme,\n          pillReady: sliding && pillReady,\n        }}\n      >\n        <div\n          ref={groupRef}\n          role={sliding ? \"radiogroup\" : \"group\"}\n          className={cn(\n            \"inline-flex flex-wrap items-center gap-1\",\n            variant === \"default\" &&\n              \"p-1 rounded-xl bg-muted border border-border/50\",\n            variant === \"segmented\" &&\n              cn(\n                \"relative flex-nowrap gap-0 rounded-full p-1\",\n                SEGMENTED_TRACK,\n                segmentedHeights[size],\n              ),\n            className,\n          )}\n          style={{ ...style, ...styleProp }}\n          onKeyDown={handleKeyDown}\n          {...props}\n        >\n          {sliding && (\n            <span\n              ref={pillRef}\n              aria-hidden=\"true\"\n              className={cn(\n                \"pointer-events-none absolute left-0 top-0 rounded-full opacity-0 transition-[transform,width,height] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none\",\n                SEGMENTED_PILL,\n              )}\n            />\n          )}\n          {children}\n        </div>\n      </ToggleGroupContext.Provider>\n    );\n  },\n);\nToggleGroupImpl.displayName = \"ToggleGroup\";\n\n/**\n * The implementation is written against the collapsed props; this cast is\n * what hands callers the discriminated pair instead.\n */\nexport const ToggleGroup = ToggleGroupImpl as React.ForwardRefExoticComponent<\n  ToggleGroupProps & React.RefAttributes<HTMLDivElement>\n>;\n\n// ==========================================\n// ITEM COMPONENT\n// ==========================================\nexport interface ToggleGroupItemProps\n  extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n  value: string;\n}\n\nexport const ToggleGroupItem = forwardRef<\n  HTMLButtonElement,\n  ToggleGroupItemProps\n>(({ value, disabled: itemDisabled, className, children, ...props }, ref) => {\n  const context = useToggleGroup();\n  const {\n    type,\n    value: groupValue,\n    onValueChange,\n    variant,\n    size,\n    disabled: groupDisabled,\n    theme,\n    pillReady,\n  } = context;\n\n  const isDisabled = groupDisabled || itemDisabled;\n  const segmented = variant === \"segmented\";\n  const radio = segmented && type === \"single\";\n\n  // Check if active\n  const isActive =\n    type === \"single\"\n      ? groupValue === value\n      : Array.isArray(groupValue) && groupValue.includes(value);\n\n  const handleToggle = () => {\n    if (isDisabled) return;\n\n    if (type === \"single\") {\n      // A segmented choice is a radio: pressing it again keeps it.\n      if (radio) {\n        if (!isActive) onValueChange(value);\n        return;\n      }\n      onValueChange(isActive ? \"\" : value);\n    } else {\n      const currentArray = Array.isArray(groupValue) ? groupValue : [];\n      if (isActive) {\n        onValueChange(currentArray.filter((v) => v !== value));\n      } else {\n        onValueChange([...currentArray, value]);\n      }\n    }\n  };\n\n  const sizeStyles = {\n    sm: \"h-8 px-2.5 text-xs\",\n    md: \"h-10 px-3.5 text-sm\",\n    lg: \"h-12 px-5 text-base\",\n  };\n\n  const segmentedSizes = {\n    sm: \"h-full px-3 text-[13px]\",\n    md: \"h-full px-4 text-sm\",\n    lg: \"h-full px-5 text-base\",\n  };\n\n  const getVariantStyles = () => {\n    if (segmented) {\n      return cn(\n        \"relative z-[1] whitespace-nowrap rounded-full text-muted-foreground transition-colors hover:text-foreground focus-visible:ring-primary/60 focus-visible:ring-offset-0\",\n        segmentedSizes[size],\n        isActive && \"text-foreground\",\n        // Until the sliding highlight is measured (and in multiple groups,\n        // which have no single highlight) the pressed item paints its own.\n        isActive && !pillReady && SEGMENTED_PILL,\n      );\n    }\n    if (isActive) {\n      switch (variant) {\n        case \"outline\":\n          return cn(theme.border, \"shadow-sm\");\n        case \"ghost\":\n          return cn(theme.text, \"bg-muted\");\n        case \"default\":\n        default:\n          return cn(theme.bg, \"shadow-md\");\n      }\n    }\n\n    // Inactive states\n    switch (variant) {\n      case \"outline\":\n        return \"border border-border bg-transparent text-muted-foreground hover:bg-muted\";\n      case \"ghost\":\n        return \"bg-transparent text-muted-foreground hover:bg-muted\";\n      case \"default\":\n      default:\n        return \"bg-transparent text-muted-foreground hover:bg-muted/50\";\n    }\n  };\n\n  return (\n    <button\n      ref={ref}\n      type=\"button\"\n      role={type === \"single\" ? \"radio\" : \"checkbox\"}\n      aria-checked={isActive}\n      disabled={isDisabled}\n      data-toggle-item=\"\"\n      data-state={isActive ? \"on\" : \"off\"}\n      data-value={value}\n      // A segmented radio group is one tab stop; the arrow keys move inside it.\n      tabIndex={radio ? (isActive || groupValue === \"\" ? 0 : -1) : undefined}\n      onClick={handleToggle}\n      className={cn(\n        \"inline-flex items-center justify-center gap-2 rounded-lg font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 focus-visible:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed\",\n        sizeStyles[size],\n        getVariantStyles(),\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </button>\n  );\n});\nToggleGroupItem.displayName = \"ToggleGroupItem\";\n",
      "type": "registry:component",
      "target": "components/custom/blocks/ToggleGroupBlock.tsx"
    },
    {
      "path": "src/components/custom/blocks/toggle-group-theme.ts",
      "content": "\"use client\";\n\nimport React from \"react\";\n\nconst colorThemes: Record<string, Record<string, string>> = {\n  // The theme's own accent, so a group follows whatever `--primary` the\n  // consumer set — the same contract Button and Input honour.\n  primary: {\n    bg: \"bg-primary text-primary-foreground\",\n    text: \"text-primary\",\n    border: \"border-primary/30 text-primary bg-primary/10\",\n  },\n  indigo: {\n    bg: \"bg-indigo-500 text-white\",\n    text: \"text-indigo-600 dark:text-indigo-400\",\n    border:\n      \"border-indigo-500 text-indigo-600 dark:text-indigo-400 bg-indigo-50 dark:bg-indigo-500/10\",\n  },\n  rose: {\n    bg: \"bg-rose-500 text-white\",\n    text: \"text-rose-600 dark:text-rose-400\",\n    border:\n      \"border-rose-500 text-rose-600 dark:text-rose-400 bg-rose-50 dark:bg-rose-500/10\",\n  },\n  emerald: {\n    bg: \"bg-emerald-500 text-white\",\n    text: \"text-emerald-600 dark:text-emerald-400\",\n    border:\n      \"border-emerald-500 text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-500/10\",\n  },\n  zinc: {\n    bg: \"bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900\",\n    text: \"text-zinc-900 dark:text-zinc-100\",\n    border:\n      \"border-zinc-900 dark:border-zinc-100 text-zinc-900 dark:text-zinc-100 bg-zinc-100 dark:bg-zinc-800\",\n  },\n};\n\nconst hexToRgb = (hex: string) => {\n  let cleanHex = hex.replace(/^#/, \"\");\n  if (cleanHex.length === 3)\n    cleanHex = cleanHex\n      .split(\"\")\n      .map((x) => x + x)\n      .join(\"\");\n  const result = /^([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(cleanHex);\n  return result\n    ? `${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}`\n    : \"99, 102, 241\";\n};\n\nexport const getThemeStyles = (color: string) => {\n  if (color && color.startsWith(\"#\")) {\n    const rgb = hexToRgb(color);\n    return {\n      theme: {\n        bg: \"bg-[var(--toggle-color)] text-white\",\n        text: \"text-[var(--toggle-color)]\",\n        border:\n          \"border-[var(--toggle-color)] text-[var(--toggle-color)] bg-[rgba(var(--toggle-rgb),0.1)]\",\n      },\n      style: {\n        \"--toggle-color\": color,\n        \"--toggle-rgb\": rgb,\n      } as React.CSSProperties,\n    };\n  }\n  return { theme: colorThemes[color] || colorThemes.primary, style: {} };\n};\n",
      "type": "registry:component",
      "target": "components/custom/blocks/toggle-group-theme.ts"
    }
  ],
  "docs": "Named exports: ToggleGroup, ToggleGroupItem, getThemeStyles. Set `type` to single or multiple — `value` is a string for single, an array for multiple. Compose ToggleGroupItem children; the group passes theme and size down. `color` accepts primary (the theme token), indigo, rose, emerald, zinc or a hex. `variant=\"segmented\"` is a pill track whose highlight slides to the pressed item; a single segmented group keeps its choice when clicked again and moves it with the arrow keys.",
  "categories": [
    "forms",
    "actions"
  ],
  "type": "registry:component"
}
