{"$schema":"https:\/\/ui.particle.academy\/schema\/registry-item.json","name":"multi-switch","type":"registry:ui","title":"MultiSwitch","description":"Segmented toggle switch for selecting one value from multiple options.","package":"react-fancy","dependencies":["tailwind-merge"],"registryDependencies":["field"],"files":[{"path":"components\/fancy\/multi-switch\/MultiSwitch.colors.ts","content":"\/\/ Literal Tailwind class strings for every color in the v4 palette so the\n\/\/ scanner can detect them in the published dist (template-built names are\n\/\/ invisible to Tailwind's JIT). Shades follow badgeSolid: 600 \/ dark 500 for\n\/\/ the thumb, with dark label text on the light hues (amber, yellow, lime).\nimport type { Color } from \"..\/lib\/utils\/types\";\n\nexport type MultiSwitchColorClasses = {\n  \/** The selected thumb's fill. *\/\n  thumb: string;\n  \/** The selected option's label, readable on the thumb. *\/\n  text: string;\n  \/** The container border while this option is selected. *\/\n  border: string;\n};\n\nexport const multiSwitchColorClasses: Record<Color, MultiSwitchColorClasses> = {\n  \/\/ The host's colour, from CSS variables -- no dark: variant, because a\n  \/\/ host that themes per mode scopes the variables themselves.\n  brand: { thumb: \"bg-brand\", text: \"text-brand-contrast\", border: \"border-brand\" },\n  slate: { thumb: \"bg-slate-600 dark:bg-slate-500\", text: \"text-white\", border: \"border-slate-500 dark:border-slate-400\" },\n  gray: { thumb: \"bg-gray-600 dark:bg-gray-500\", text: \"text-white\", border: \"border-gray-500 dark:border-gray-400\" },\n  zinc: { thumb: \"bg-zinc-600 dark:bg-zinc-500\", text: \"text-white\", border: \"border-zinc-500 dark:border-zinc-400\" },\n  neutral: { thumb: \"bg-neutral-600 dark:bg-neutral-500\", text: \"text-white\", border: \"border-neutral-500 dark:border-neutral-400\" },\n  stone: { thumb: \"bg-stone-600 dark:bg-stone-500\", text: \"text-white\", border: \"border-stone-500 dark:border-stone-400\" },\n  red: { thumb: \"bg-red-600 dark:bg-red-500\", text: \"text-white\", border: \"border-red-500 dark:border-red-400\" },\n  orange: { thumb: \"bg-orange-600 dark:bg-orange-500\", text: \"text-white\", border: \"border-orange-500 dark:border-orange-400\" },\n  amber: { thumb: \"bg-amber-400 dark:bg-amber-300\", text: \"text-amber-950\", border: \"border-amber-500 dark:border-amber-400\" },\n  yellow: { thumb: \"bg-yellow-400 dark:bg-yellow-300\", text: \"text-yellow-950\", border: \"border-yellow-500 dark:border-yellow-400\" },\n  lime: { thumb: \"bg-lime-400 dark:bg-lime-300\", text: \"text-lime-950\", border: \"border-lime-500 dark:border-lime-400\" },\n  green: { thumb: \"bg-green-600 dark:bg-green-500\", text: \"text-white\", border: \"border-green-500 dark:border-green-400\" },\n  emerald: { thumb: \"bg-emerald-600 dark:bg-emerald-500\", text: \"text-white\", border: \"border-emerald-500 dark:border-emerald-400\" },\n  teal: { thumb: \"bg-teal-600 dark:bg-teal-500\", text: \"text-white\", border: \"border-teal-500 dark:border-teal-400\" },\n  cyan: { thumb: \"bg-cyan-600 dark:bg-cyan-500\", text: \"text-white\", border: \"border-cyan-500 dark:border-cyan-400\" },\n  sky: { thumb: \"bg-sky-600 dark:bg-sky-500\", text: \"text-white\", border: \"border-sky-500 dark:border-sky-400\" },\n  blue: { thumb: \"bg-blue-600 dark:bg-blue-500\", text: \"text-white\", border: \"border-blue-500 dark:border-blue-400\" },\n  indigo: { thumb: \"bg-indigo-600 dark:bg-indigo-500\", text: \"text-white\", border: \"border-indigo-500 dark:border-indigo-400\" },\n  violet: { thumb: \"bg-violet-600 dark:bg-violet-500\", text: \"text-white\", border: \"border-violet-500 dark:border-violet-400\" },\n  purple: { thumb: \"bg-purple-600 dark:bg-purple-500\", text: \"text-white\", border: \"border-purple-500 dark:border-purple-400\" },\n  fuchsia: { thumb: \"bg-fuchsia-600 dark:bg-fuchsia-500\", text: \"text-white\", border: \"border-fuchsia-500 dark:border-fuchsia-400\" },\n  pink: { thumb: \"bg-pink-600 dark:bg-pink-500\", text: \"text-white\", border: \"border-pink-500 dark:border-pink-400\" },\n  rose: { thumb: \"bg-rose-600 dark:bg-rose-500\", text: \"text-white\", border: \"border-rose-500 dark:border-rose-400\" },\n};\n","type":"registry:ui","target":"components\/fancy\/multi-switch\/MultiSwitch.colors.ts"},{"path":"components\/fancy\/multi-switch\/MultiSwitch.tsx","content":"import { useRef, useState, useEffect, useCallback, useId, useMemo, type Ref } from \"react\";\nimport { cn } from \"..\/lib\/utils\/cn\";\nimport { composeRefs } from \"..\/lib\/utils\/compose-refs\";\nimport { useControllableState } from \"..\/lib\/hooks\/use-controllable-state\";\nimport { Field } from \"..\/field\";\nimport { useFieldMode } from \"..\/lib\/components\/inputs\/mode\/FieldMode.context\";\nimport { DisplayValue } from \"..\/lib\/components\/inputs\/mode\/DisplayValue\";\nimport { useInlineEdit } from \"..\/lib\/components\/inputs\/mode\/useInlineEdit\";\nimport { resolveOption } from \"..\/lib\/components\/inputs\/inputs.utils\";\nimport type { Color, Size } from \"..\/lib\/utils\/types\";\nimport type { MultiSwitchProps } from \".\/MultiSwitch.types\";\nimport { multiSwitchColorClasses } from \".\/MultiSwitch.colors\";\n\nconst BORDER_DEFAULT =\n  \"border-[color:var(--fancy-border-strong,var(--color-zinc-300))] dark:border-[color:var(--fancy-border-strong-dark,var(--color-zinc-700))]\";\n\nconst sizeClasses: Record<Size, string> = {\n  xs: \"px-2 py-1 text-xs\",\n  sm: \"px-2.5 py-1.5 text-sm\",\n  md: \"px-3 py-2 text-sm\",\n  lg: \"px-4 py-2.5 text-base\",\n  xl: \"px-5 py-3 text-lg\",\n};\n\nexport function MultiSwitch<V = string>({\n  size = \"md\",\n  dirty,\n  error,\n  label,\n  labelHidden,\n  description,\n  required,\n  disabled,\n  className,\n  id,\n  name,\n  list,\n  value: controlledValue,\n  defaultValue,\n  onValueChange,\n  linear,\n  mode,\n  ...rest\n}: MultiSwitchProps<V>) {\n  const reactId = useId();\n  \/\/ A `<label for>` cannot name a div[role=\"radiogroup\"], so the group points at\n  \/\/ the label instead. Needs an id the label and the group both agree on.\n  const labelId = label ? `${id ?? reactId}-label` : undefined;\n\n  \/\/ resolveOption hands an object option back as-is, so `color` survives it.\n  const resolvedOptions = list.map(resolveOption) as Array<\n    ReturnType<typeof resolveOption<V>> & { color?: Color }\n  >;\n  const fallback = defaultValue ?? resolvedOptions[0]?.value;\n  const resolvedMode = useFieldMode(mode);\n  const { showControl, interactive, enterEdit, exitEdit } = useInlineEdit(resolvedMode, disabled);\n  const [value, setValue] = useControllableState(controlledValue, fallback as V, onValueChange);\n\n  const containerRef = useRef<HTMLDivElement>(null);\n  const externalRef = (rest as { ref?: Ref<HTMLDivElement> }).ref;\n  const containerSetRef = useMemo(() => composeRefs(externalRef, containerRef), [externalRef]);\n  const itemRefs = useRef<(HTMLButtonElement | null)[]>([]);\n  const [indicator, setIndicator] = useState<{ left: number; width: number } | null>(null);\n\n  const selectedIndex = resolvedOptions.findIndex(\n    (o) => o.value === value,\n  );\n  const selectedColor = resolvedOptions[selectedIndex]?.color;\n  const colorClasses = selectedColor ? multiSwitchColorClasses[selectedColor] : undefined;\n\n  const updateIndicator = useCallback(() => {\n    const container = containerRef.current;\n    const activeItem = itemRefs.current[selectedIndex];\n    if (!container || !activeItem) {\n      setIndicator(null);\n      return;\n    }\n    const containerRect = container.getBoundingClientRect();\n    const itemRect = activeItem.getBoundingClientRect();\n    setIndicator({\n      left: itemRect.left - containerRect.left,\n      width: itemRect.width,\n    });\n  }, [selectedIndex]);\n\n  useEffect(() => {\n    updateIndicator();\n  }, [updateIndicator]);\n\n  const control = !showControl ? (\n    <DisplayValue size={size} interactive={interactive} onActivate={enterEdit}>\n      {resolvedOptions.find((o) => o.value === value)?.label}\n    <\/DisplayValue>\n  ) : (\n    <div\n      {...rest}\n      \/\/ React 19 passes `ref` to a function component as a plain prop, so a\n      \/\/ caller's ref (Tooltip's anchor, or anyone's) arrives in `rest`. Spread\n      \/\/ after `ref={containerRef}` it REPLACED the ref the thumb is measured\n      \/\/ from, and the selected thumb never rendered. Feed both instead.\n      ref={containerSetRef}\n      data-react-fancy-multi-switch=\"\"\n      role=\"radiogroup\"\n      id={id}\n      aria-labelledby={labelId}\n      onBlur={(e) => {\n        if (interactive && !e.currentTarget.contains(e.relatedTarget as Node)) exitEdit();\n      }}\n      className={cn(\n        \"relative inline-flex rounded-lg border bg-[color:var(--fancy-surface-muted,var(--color-zinc-100))] dark:bg-[color:var(--fancy-surface-muted-dark,var(--color-zinc-800))]\",\n        colorClasses ? colorClasses.border : BORDER_DEFAULT,\n        dirty && \"ring-2 ring-amber-400\/50\",\n        error && \"ring-2 ring-red-500\/50\",\n        disabled && \"opacity-50 cursor-not-allowed\",\n        className,\n      )}\n    >\n      {indicator && (\n        <span\n          data-react-fancy-multi-switch-thumb=\"\"\n          data-color={selectedColor}\n          className={\n            colorClasses\n              ? `absolute top-0 bottom-0 rounded-lg ${colorClasses.thumb} transition-all duration-200 ease-out`\n              : \"absolute top-0 bottom-0 rounded-lg bg-blue-600 transition-all duration-200 ease-out\"\n          }\n          style={{ left: indicator.left, width: indicator.width }}\n        \/>\n      )}\n      {resolvedOptions.map((option, index) => {\n        const isSelected = option.value === value;\n        return (\n          <button\n            key={String(option.value)}\n            ref={(el) => { itemRefs.current[index] = el; }}\n            type=\"button\"\n            role=\"radio\"\n            aria-checked={isSelected}\n            disabled={disabled || option.disabled}\n            autoFocus={interactive && (selectedIndex === -1 ? index === 0 : isSelected)}\n            onClick={() => {\n              if (linear) {\n                const nextIndex = (selectedIndex + 1) % resolvedOptions.length;\n                setValue(resolvedOptions[nextIndex].value as V);\n              } else {\n                setValue(option.value as V);\n              }\n            }}\n            className={cn(\n              \"relative z-10 font-medium transition-colors duration-200\",\n              sizeClasses[size],\n              isSelected\n                ? (colorClasses?.text ?? \"text-white\")\n                : \"text-[color:var(--fancy-text-subtle,var(--color-zinc-600))] hover:text-[color:var(--fancy-text,var(--color-zinc-900))] dark:text-[color:var(--fancy-text-subtle-dark,var(--color-zinc-400))] dark:hover:text-[color:var(--fancy-text-dark,var(--color-zinc-100))]\",\n              (disabled || option.disabled) && \"cursor-not-allowed\",\n            )}\n          >\n            {option.label}\n          <\/button>\n        );\n      })}\n      {name && <input type=\"hidden\" name={name} value={String(value)} \/>}\n    <\/div>\n  );\n\n  if (label || error || description) {\n    return (\n      <Field\n        label={label}\n        labelHidden={labelHidden}\n        labelId={labelId}\n        description={description}\n        error={error}\n        required={required}\n        htmlFor={id}\n        size={size}\n      >\n        {control}\n      <\/Field>\n    );\n  }\n\n  return control;\n}\n\nMultiSwitch.displayName = \"MultiSwitch\";\n","type":"registry:ui","target":"components\/fancy\/multi-switch\/MultiSwitch.tsx"},{"path":"components\/fancy\/multi-switch\/MultiSwitch.types.ts","content":"import type { HTMLAttributes } from \"react\";\nimport type { Color } from \"..\/lib\/utils\/types\";\nimport type { InputBaseProps, InputOption } from \"..\/lib\/components\/inputs\/inputs.types\";\n\n\/**\n * An option, optionally with its own `color`: while it is selected the thumb\n * and the container border take that colour (a green \"Assign\" against a red\n * \"Unassign\"). An option without one keeps the default blue thumb.\n *\/\nexport type MultiSwitchOption<V = string> =\n  | InputOption<V>\n  | { value: V; label: string; disabled?: boolean; description?: string; color?: Color };\n\n\/**\n * The container's DOM attributes are part of the API because the component\n * already spreads its rest props onto that container \u2014 `style`, `data-*` and\n * `aria-*` all worked at runtime while the type refused them, so a caller got a\n * compiler error for something the component does correctly.\n *\n * `onChange` is omitted because this control reports through `onValueChange`,\n * and admitting both would offer a handler that never fires.\n *\/\nexport interface MultiSwitchProps<V = string>\n  extends InputBaseProps,\n    Omit<HTMLAttributes<HTMLDivElement>, \"onChange\" | \"defaultValue\"> {\n  list: MultiSwitchOption<V>[];\n  value?: V;\n  defaultValue?: V;\n  onValueChange?: (value: V) => void;\n  linear?: boolean;\n}\n","type":"registry:ui","target":"components\/fancy\/multi-switch\/MultiSwitch.types.ts"},{"path":"components\/fancy\/multi-switch\/index.ts","content":"export { MultiSwitch } from \".\/MultiSwitch\";\nexport type { MultiSwitchProps, MultiSwitchOption } from \".\/MultiSwitch.types\";\n","type":"registry:ui","target":"components\/fancy\/multi-switch\/index.ts"},{"path":"components\/fancy\/lib\/utils\/types.ts","content":"import type { ElementType } from \"react\";\n\nexport type Size = \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\";\n\n\/**\n * What a navigation primitive owes a router.\n *\n * Every nav surface here \u2014 `Navbar.Item`, `Sidebar.Item`, `Menu.Item`,\n * `MobileMenu.Item`, `Breadcrumbs.Item` \u2014 used to hardcode a plain `<a href>`.\n * That is a FULL PAGE LOAD in every client-routed app, and the only way around\n * it was to nest a router `<Link>` inside the item: an anchor inside an anchor,\n * which is invalid HTML and the same nested-anchor shape that already cost this\n * suite an SSR hydration bug.\n *\n * `as` is the seam {@link ButtonProps.as} already had. It is router-agnostic on\n * purpose \u2014 one prop serves Inertia, TanStack Router, React Router and Next\n * rather than a package per router:\n *\n * ```tsx\n * \/\/ href-based routers (Inertia, Next)\n * <Navbar.Item as={Link} href=\"\/dashboard\">Dashboard<\/Navbar.Item>\n *\n * \/\/ to-based routers (TanStack Router, React Router) \u2014 `to` reaches the Link\n * \/\/ through the forwarded rest props\n * <Navbar.Item as={Link} to=\"\/dashboard\">Dashboard<\/Navbar.Item>\n * ```\n *\n * Link mode engages on `href` OR `as`, because a `to`-based router never passes\n * an `href` and would otherwise silently render the non-interactive fallback.\n *\/\nexport interface NavigableProps {\n  \/** Target URL. Renders link mode with a plain `<a>` unless `as` is given. *\/\n  href?: string;\n  \/**\n   * Link-mode element override \u2014 the component to render INSTEAD of the plain\n   * `<a>`, e.g. a router's `<Link>` so navigation happens client-side. Receives\n   * `className`, the item's `data-*` handles, and every unrecognized prop, so\n   * router-specific props (`to`, `params`, `preload`, `search`) pass straight\n   * through. Ignored in button mode.\n   *\/\n  as?: ElementType;\n}\n\n\/**\n * The full Tailwind v4 named color palette \u2014 every default hue, including all\n * five gray families \u2014 plus `brand`, the host's own colour. Components that\n * accept a `color` should type it as this so the whole palette is available.\n *\n * `brand` is not a hue. It resolves through three CSS variables the host sets\n * once, rather than through a Tailwind scale:\n *\n *   `--color-brand`, `--color-brand-contrast`, `--color-brand-hover`\n *\n * Three rather than one because a generated hover assumes \"same hue, darker\",\n * and a brand palette need not work that way \u2014 navy whose hover is gold cannot\n * be derived from navy by any amount of arithmetic. The host states it.\n *\/\nexport type Color =\n  \/\/ the host's own colour, resolved from CSS variables (see styles.css)\n  | \"brand\"\n  \/\/ grays\n  | \"slate\"\n  | \"gray\"\n  | \"zinc\"\n  | \"neutral\"\n  | \"stone\"\n  \/\/ hues\n  | \"red\"\n  | \"orange\"\n  | \"amber\"\n  | \"yellow\"\n  | \"lime\"\n  | \"green\"\n  | \"emerald\"\n  | \"teal\"\n  | \"cyan\"\n  | \"sky\"\n  | \"blue\"\n  | \"indigo\"\n  | \"violet\"\n  | \"purple\"\n  | \"fuchsia\"\n  | \"pink\"\n  | \"rose\";\n\n\/** Every value of {@link Color}, for building exhaustive class maps. *\/\nexport const COLORS = [\n  \"brand\",\n  \"slate\", \"gray\", \"zinc\", \"neutral\", \"stone\",\n  \"red\", \"orange\", \"amber\", \"yellow\", \"lime\", \"green\", \"emerald\", \"teal\",\n  \"cyan\", \"sky\", \"blue\", \"indigo\", \"violet\", \"purple\", \"fuchsia\", \"pink\", \"rose\",\n] as const satisfies readonly Color[];\n\nexport type Variant = \"solid\" | \"outline\" | \"ghost\" | \"soft\";\n\n\/** Button accepts the full Tailwind v4 palette (alias of {@link Color}). *\/\nexport type ButtonColor = Color;\n\n\/**\n * @deprecated Renamed to {@link ButtonColor}. `ActionColor` remains as an alias\n * for backward compatibility and will be removed in a future major version.\n *\/\nexport type ActionColor = ButtonColor;\n\nexport type Placement =\n  | \"top\"\n  | \"bottom\"\n  | \"left\"\n  | \"right\"\n  | \"top-start\"\n  | \"top-end\"\n  | \"bottom-start\"\n  | \"bottom-end\";\n","type":"registry:lib","target":"components\/fancy\/lib\/utils\/types.ts"},{"path":"components\/fancy\/lib\/utils\/cn.ts","content":"import { twMerge } from \"tailwind-merge\";\n\n\/**\n * A class value: a string, a number, a conditional map, an array of any of\n * those, or nothing. Mirrors what `clsx` accepted, because every component in\n * this package already passes these shapes.\n *\/\nexport type ClassValue =\n  | string\n  | number\n  | bigint\n  | null\n  | undefined\n  | false\n  | ClassDictionary\n  | ClassValue[];\n\ntype ClassDictionary = Record<string, unknown>;\n\n\/**\n * Flatten class values into a space-separated string.\n *\n * This was `clsx` until 2026-08-19. It is roughly twenty lines of logic, it\n * shipped as a RUNTIME dependency to every consumer of this package, and its\n * last commit was 801 days old \u2014 so it fell to the rule that third-party code\n * must be both approved and actively maintained. Owning it costs less than\n * carrying it.\n *\n * `tailwind-merge` deliberately stays: deciding which of two conflicting\n * Tailwind utilities wins requires modelling the entire utility space, which is\n * emphatically not twenty lines, and it is actively maintained.\n *\n * Falsy values are dropped \u2014 including `0` and `\"\"`, which is what `clsx` did\n * and what `tests\/cn.test.ts` pins. A truthy number is kept, because digits are\n * legal class names. `bigint` is admitted because clsx's signature did and\n * the compiler proved it reachable from two call sites here.\n *\/\nfunction flatten(value: ClassValue): string {\n  if (!value) return \"\";\n\n  if (typeof value === \"string\" || typeof value === \"number\" || typeof value === \"bigint\") {\n    return String(value);\n  }\n\n  if (Array.isArray(value)) {\n    let out = \"\";\n    for (const item of value) {\n      const part = flatten(item);\n      if (part) out = out ? `${out} ${part}` : part;\n    }\n    return out;\n  }\n\n  let out = \"\";\n  for (const key in value) {\n    if (value[key]) out = out ? `${out} ${key}` : key;\n  }\n  return out;\n}\n\nexport function cn(...inputs: ClassValue[]): string {\n  return twMerge(flatten(inputs));\n}\n","type":"registry:lib","target":"components\/fancy\/lib\/utils\/cn.ts"},{"path":"components\/fancy\/lib\/utils\/compose-refs.ts","content":"import type { Ref, RefCallback } from \"react\";\n\n\/**\n * One ref callback that feeds every ref it is given.\n *\n * For a component that needs its own handle on an element AND has to honour a\n * ref someone else put there \u2014 a Tooltip anchoring to its trigger, a control\n * measuring itself while a caller holds a ref to it. Assigning one ref over the\n * other silently breaks whichever side lost.\n *\/\nexport function composeRefs<T>(...refs: Array<Ref<T> | undefined | null>): RefCallback<T> {\n  return (node: T | null) => {\n    for (const ref of refs) {\n      if (typeof ref === \"function\") ref(node);\n      else if (ref) (ref as { current: T | null }).current = node;\n    }\n  };\n}\n","type":"registry:lib","target":"components\/fancy\/lib\/utils\/compose-refs.ts"},{"path":"components\/fancy\/lib\/hooks\/use-controllable-state.ts","content":"import { useCallback, useRef, useState } from \"react\";\n\n\/**\n * One state slot that works controlled or uncontrolled.\n *\n * ---------------------------------------------------------------------------\n * Why the value is tracked in a ref (fixed 5.32.0)\n * ---------------------------------------------------------------------------\n *\n * This resolved a functional update against the RENDER CLOSURE's `value` and then\n * handed React an already-computed value:\n *\n *     const nextValue = typeof next === \"function\" ? next(value) : next;\n *     setUncontrolledValue(nextValue);\n *\n * So `setValue(fn)` twice in one batch had both calls compute from the same stale\n * `value`, and **the first update was silently lost.** React's own `setState`\n * updater form does not have that problem precisely because React resolves it\n * against the latest state; passing a pre-computed value gives that up.\n *\n * Found by a hook test that paste-held two items inside one `act()` and got one.\n * It is not an exotic case \u2014 any caller that loops, or handles two events in a\n * batch, hits it, and the symptom is a dropped update with nothing to indicate one\n * happened.\n *\n * The fix cannot be \"pass the updater to React\", because `onChange` has to be\n * called with the resulting value and an updater must stay pure \u2014 React may invoke\n * it twice (StrictMode), which would fire `onChange` twice. So the previous value\n * comes from a ref that is re-synced from the authoritative `value` on every render\n * and advanced on every set. Within a batch the ref carries intent; after a render\n * the real value wins.\n *\n * A side effect worth having: `setValue` no longer depends on `value`, so it is\n * stable for the life of the component.\n *\/\nexport function useControllableState<T>(\n  controlledValue: T | undefined,\n  defaultValue: T,\n  onChange?: (value: T) => void,\n): [T, (value: T | ((prev: T) => T)) => void] {\n  const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);\n  const isControlled = controlledValue !== undefined;\n  const value = isControlled ? controlledValue : uncontrolledValue;\n\n  const onChangeRef = useRef(onChange);\n  onChangeRef.current = onChange;\n\n  \/\/ Re-synced every render from whichever side owns the value. A controlled parent\n  \/\/ that ignores `onChange` therefore wins on the next render rather than being\n  \/\/ overridden by what this hook optimistically recorded.\n  const valueRef = useRef(value);\n  valueRef.current = value;\n\n  const isControlledRef = useRef(isControlled);\n  isControlledRef.current = isControlled;\n\n  const setValue = useCallback((next: T | ((prev: T) => T)) => {\n    const nextValue =\n      typeof next === \"function\"\n        ? (next as (prev: T) => T)(valueRef.current)\n        : next;\n\n    \/\/ Advance the ref BEFORE notifying, so a second call in the same batch builds\n    \/\/ on this one instead of on what the last render saw.\n    valueRef.current = nextValue;\n\n    if (!isControlledRef.current) {\n      setUncontrolledValue(nextValue);\n    }\n\n    onChangeRef.current?.(nextValue);\n  }, []);\n\n  return [value, setValue];\n}\n","type":"registry:lib","target":"components\/fancy\/lib\/hooks\/use-controllable-state.ts"},{"path":"components\/fancy\/lib\/components\/inputs\/mode\/FieldMode.context.ts","content":"import { createContext, useContext } from \"react\";\nimport type { FieldMode } from \"..\/inputs.types\";\n\nexport interface FieldModeContextValue {\n  mode: FieldMode;\n}\n\nexport const FieldModeContext = createContext<FieldModeContextValue | null>(null);\n\n\/**\n * Resolve the effective mode for an input. Precedence: an explicit prop wins,\n * then the nearest `<Form>` \/ `<FormProvider>` context, then `\"edit\"`.\n *\n * Deliberately tolerant (returns `\"edit\"` outside any provider) \u2014 unlike a slot\n * guard \u2014 so every input works standalone with zero context and existing call\n * sites behave identically.\n *\/\nexport function useFieldMode(explicit?: FieldMode): FieldMode {\n  const ctx = useContext(FieldModeContext);\n  return explicit ?? ctx?.mode ?? \"edit\";\n}\n","type":"registry:lib","target":"components\/fancy\/lib\/components\/inputs\/mode\/FieldMode.context.ts"},{"path":"components\/fancy\/lib\/components\/inputs\/mode\/DisplayValue.tsx","content":"import type { KeyboardEvent, ReactNode } from \"react\";\nimport { cn } from \"..\/..\/..\/utils\/cn\";\nimport type { Size } from \"..\/..\/..\/utils\/types\";\nimport { inputSizeClasses } from \"..\/inputs.utils\";\n\nexport interface DisplayValueProps {\n  \/** The formatted value to show. Empty \/ null \/ undefined renders `empty`. *\/\n  children?: ReactNode;\n  size?: Size;\n  \/** Static adornment rendered before the value (mirrors an input's `leading`\/`prefix`). *\/\n  leading?: ReactNode;\n  \/** Static adornment rendered after the value (mirrors an input's `trailing`\/`suffix`). *\/\n  trailing?: ReactNode;\n  \/** Shown when the value is empty. Default `\u2014`. *\/\n  empty?: ReactNode;\n  \/** When true the value is clickable to enter edit mode (inline click-to-edit). *\/\n  interactive?: boolean;\n  \/** Called when an interactive value is activated (click \/ Enter \/ Space). *\/\n  onActivate?: () => void;\n  className?: string;\n}\n\nfunction isEmpty(value: ReactNode): boolean {\n  return value === null || value === undefined || value === \"\";\n}\n\n\/**\n * The read-only counterpart to an `<input>`. Renders a non-interactive value\n * with the same size rhythm as the editable control (reusing\n * {@link inputSizeClasses}) but without the border \/ background \/ focus ring.\n *\n * When `interactive` (inline click-to-edit), the value becomes a focusable,\n * hoverable target that calls `onActivate` on click \/ Enter \/ Space to swap in\n * the real control. Dropped into the SAME `Field` wrapper the editable control\n * uses, so label \/ description \/ error chrome stays byte-identical between the\n * display and the editor.\n *\n * Carries `data-react-fancy-display` \/ `data-mode=\"view\"` stable handles for\n * agents and tests (Human+ contract).\n *\/\nexport function DisplayValue({\n  children,\n  size = \"md\",\n  leading,\n  trailing,\n  empty = \"\u2014\",\n  interactive,\n  onActivate,\n  className,\n}: DisplayValueProps) {\n  const empties = isEmpty(children);\n\n  const onKeyDown = interactive\n    ? (e: KeyboardEvent<HTMLDivElement>) => {\n        if (e.key === \"Enter\" || e.key === \" \") {\n          e.preventDefault();\n          onActivate?.();\n        }\n      }\n    : undefined;\n\n  return (\n    <div\n      data-react-fancy-display=\"\"\n      data-mode=\"view\"\n      role={interactive ? \"button\" : undefined}\n      tabIndex={interactive ? 0 : undefined}\n      title={interactive ? \"Click to edit\" : undefined}\n      onClick={interactive ? onActivate : undefined}\n      onKeyDown={onKeyDown}\n      className={cn(\n        \"flex w-full items-center gap-2 text-[color:var(--fancy-text,var(--color-zinc-900))] dark:text-[color:var(--fancy-text-dark,var(--color-zinc-100))]\",\n        inputSizeClasses[size],\n        \/\/ Strip the editable box look \u2014 keep only the size\/padding rhythm.\n        \"border-0 bg-transparent px-0\",\n        interactive &&\n          \"-mx-2 cursor-text rounded-md px-2 outline-none transition-colors hover:bg-[color:var(--fancy-surface-muted,var(--color-zinc-100))] focus-visible:ring-2 focus-visible:ring-blue-500\/40 dark:hover:bg-[color:var(--fancy-surface-muted-dark,var(--color-zinc-800))]\",\n        empties && \"text-[color:var(--fancy-text-faint,var(--color-zinc-400))] dark:text-[color:var(--fancy-text-faint-dark,var(--color-zinc-500))]\",\n        className,\n      )}\n    >\n      {leading && <span className=\"text-[color:var(--fancy-text-faint,var(--color-zinc-400))] dark:text-[color:var(--fancy-text-faint-dark,var(--color-zinc-500))]\">{leading}<\/span>}\n      <span className=\"min-w-0 truncate\">{empties ? empty : children}<\/span>\n      {trailing && <span className=\"text-[color:var(--fancy-text-faint,var(--color-zinc-400))] dark:text-[color:var(--fancy-text-faint-dark,var(--color-zinc-500))]\">{trailing}<\/span>}\n    <\/div>\n  );\n}\n","type":"registry:lib","target":"components\/fancy\/lib\/components\/inputs\/mode\/DisplayValue.tsx"},{"path":"components\/fancy\/lib\/components\/inputs\/mode\/useInlineEdit.ts","content":"import { useCallback, useState } from \"react\";\nimport type { FieldMode } from \"..\/inputs.types\";\n\nexport interface InlineEdit {\n  \/** Render the editable control (`true`) vs. the read-only value display (`false`). *\/\n  showControl: boolean;\n  \/** Whether view mode is interactive (click-to-edit) \u2014 i.e. `view` and not disabled. *\/\n  interactive: boolean;\n  \/** Enter edit mode (no-op unless interactive). *\/\n  enterEdit: () => void;\n  \/** Leave edit mode, back to the value display. *\/\n  exitEdit: () => void;\n}\n\n\/**\n * Inline click-to-edit for `mode=\"view\"`. The bound value shows as clean static\n * text; clicking it swaps in the focused control; blur returns to the display\n * (the control's `onChange` \/ `onBlur` fire as usual, so a host can persist on\n * commit). `mode=\"edit\"` is always the control; a **disabled** view stays static\n * text (no click-to-edit).\n *\n * This is the value of \"view mode\": tidy read-only surfaces where each field\n * becomes editable only when the user reaches for it.\n *\/\nexport function useInlineEdit(mode: FieldMode, disabled?: boolean): InlineEdit {\n  const [editing, setEditing] = useState(false);\n  const interactive = mode === \"view\" && !disabled;\n  const showControl = mode !== \"view\" || (interactive && editing);\n  const enterEdit = useCallback(() => {\n    if (interactive) setEditing(true);\n  }, [interactive]);\n  const exitEdit = useCallback(() => setEditing(false), []);\n  return { showControl, interactive, enterEdit, exitEdit };\n}\n","type":"registry:lib","target":"components\/fancy\/lib\/components\/inputs\/mode\/useInlineEdit.ts"},{"path":"components\/fancy\/lib\/components\/inputs\/inputs.utils.ts","content":"import type { Size } from \"..\/..\/utils\/types\";\nimport type { InputOption } from \".\/inputs.types\";\n\nexport const inputSizeClasses: Record<Size, string> = {\n  xs: \"px-2 py-1 text-xs rounded\",\n  sm: \"px-2.5 py-1.5 text-sm rounded-md\",\n  md: \"px-3 py-2 text-sm rounded-lg\",\n  lg: \"px-4 py-2.5 text-base rounded-lg\",\n  xl: \"px-5 py-3 text-lg rounded-xl\",\n};\n\nexport const labelSizeClasses: Record<Size, string> = {\n  xs: \"text-xs\",\n  sm: \"text-xs\",\n  md: \"text-sm\",\n  lg: \"text-sm\",\n  xl: \"text-base\",\n};\n\nexport function dirtyClasses(dirty?: boolean): string {\n  return dirty ? \"border-l-[3px] border-l-amber-400\" : \"\";\n}\n\nexport function dirtyRingClasses(dirty?: boolean): string {\n  return dirty ? \"ring-2 ring-amber-400\/50\" : \"\";\n}\n\nexport function errorClasses(error?: string): string {\n  return error ? \"border-red-500 focus:ring-red-500\" : \"\";\n}\n\nexport const inputBaseClasses =\n  \"border border-[color:var(--fancy-border-strong,var(--color-zinc-300))] bg-[color:var(--fancy-input,var(--color-white))] text-[color:var(--fancy-text,var(--color-zinc-900))] placeholder:text-[color:var(--fancy-text-faint,var(--color-zinc-400))] transition-[border-color,box-shadow] duration-150 focus:outline-none focus:ring-2 focus:ring-[color:var(--fancy-focus,var(--color-blue-500))]\/40 focus:border-[color:var(--fancy-focus,var(--color-blue-500))] disabled:opacity-50 disabled:cursor-not-allowed dark:border-[color:var(--fancy-border-strong-dark,var(--color-zinc-700))] dark:bg-[color:var(--fancy-input-dark,#1e1e24)] dark:text-[color:var(--fancy-text-dark,var(--color-zinc-100))] dark:placeholder:text-[color:var(--fancy-text-faint-dark,var(--color-zinc-500))] dark:focus:border-[color:var(--fancy-focus-dark,var(--color-blue-400))] dark:focus:ring-[color:var(--fancy-focus-dark,var(--color-blue-400))]\/20\";\n\nexport function resolveOption<V = string>(\n  option: InputOption<V>,\n): { value: V; label: string; disabled?: boolean; description?: string } {\n  if (typeof option === \"string\") {\n    return { value: option as unknown as V, label: option };\n  }\n  return option;\n}\n","type":"registry:lib","target":"components\/fancy\/lib\/components\/inputs\/inputs.utils.ts"},{"path":"components\/fancy\/lib\/components\/inputs\/inputs.types.ts","content":"import type { ReactNode } from \"react\";\nimport type { Size } from \"..\/..\/utils\/types\";\n\nexport type InputOption<V = string> =\n  | string\n  | { value: V; label: string; disabled?: boolean; description?: string };\n\nexport type InputOptionGroup<V = string> = {\n  label: string;\n  options: InputOption<V>[];\n};\n\n\/**\n * Edit vs. read-only display. In `\"view\"` mode an input renders the bound value\n * as static text instead of an interactive control \u2014 the React\/Inertia analog of\n * Livewire's public\/bindable properties. Declared here (rather than in `mode\/`)\n * to keep `InputBaseProps` free of an import cycle. Resolved per input via\n * {@link useFieldMode}: explicit prop \u2192 `<Form>`\/`<FormProvider>` context \u2192 `\"edit\"`.\n *\/\nexport type FieldMode = \"edit\" | \"view\";\n\nexport interface InputBaseProps {\n  size?: Size;\n  dirty?: boolean;\n  error?: string;\n  label?: string;\n  \/**\n   * Keep the label for assistive tech, but do not draw it.\n   *\n   * For a control whose options already say what it does \u2014 a filter toolbar's\n   * segmented switches, say \u2014 where a visible label is noise but an unnamed\n   * control is a bug. Without this the only choices were an unnamed control or\n   * a label the design does not have, and the silent one is the one that ships.\n   *\/\n  labelHidden?: boolean;\n  description?: string;\n  required?: boolean;\n  disabled?: boolean;\n  className?: string;\n  id?: string;\n  name?: string;\n  \/** `\"edit\"` (default) renders the control; `\"view\"` renders the value as text. Overrides the `<Form mode>` context. *\/\n  mode?: FieldMode;\n}\n\nexport type AffixPosition = \"inside\" | \"outside\";\n\nexport interface InputAffixProps {\n  prefix?: ReactNode;\n  suffix?: ReactNode;\n  prefixPosition?: AffixPosition;\n  suffixPosition?: AffixPosition;\n}\n","type":"registry:lib","target":"components\/fancy\/lib\/components\/inputs\/inputs.types.ts"}]}