{"$schema":"https:\/\/ui.particle.academy\/schema\/registry-item.json","name":"slider","type":"registry:ui","title":"Slider","description":"Single-value or range slider with marks and visible value formatting.","package":"react-fancy","dependencies":["tailwind-merge"],"registryDependencies":["field"],"files":[{"path":"components\/fancy\/slider\/Slider.tsx","content":"import { forwardRef, useId } from \"react\";\nimport { cn } from \"..\/lib\/utils\/cn\";\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 { dirtyRingClasses } from \"..\/lib\/components\/inputs\/inputs.utils\";\nimport type { SliderProps } from \".\/Slider.types\";\n\nexport const Slider = forwardRef<HTMLInputElement, SliderProps>(\n  (props, ref) => {\n    const {\n      size = \"md\",\n      dirty,\n      error,\n      label,\n      labelHidden,\n      description,\n      required,\n      disabled,\n      className,\n      id,\n      name,\n      min = 0,\n      max = 100,\n      step = 1,\n      showValue,\n      marks,\n    } = props;\n\n    const autoId = useId();\n    const sliderId = id ?? autoId;\n\n    if (props.range) {\n      return (\n        <RangeSlider\n          {...props}\n          id={sliderId}\n          ref={ref}\n        \/>\n      );\n    }\n\n    return (\n      <SingleSlider\n        {...props}\n        range={false}\n        id={sliderId}\n        ref={ref}\n      \/>\n    );\n  },\n);\n\nSlider.displayName = \"Slider\";\n\nconst SingleSlider = forwardRef<HTMLInputElement, SliderProps & { range?: false }>(\n  (\n    {\n      size = \"md\",\n      dirty,\n      error,\n      label,\n      labelHidden,\n      description,\n      required,\n      disabled,\n      className,\n      id,\n      name,\n      min = 0,\n      max = 100,\n      step = 1,\n      showValue,\n      marks,\n      prefix,\n      suffix,\n      mode,\n      ...rest\n    },\n    ref,\n  ) => {\n    const singleProps = rest as { value?: number; defaultValue?: number; onValueChange?: (v: number) => void };\n    const resolvedMode = useFieldMode(mode);\n    const { showControl, interactive, enterEdit, exitEdit } = useInlineEdit(resolvedMode, disabled);\n    const [value, setValue] = useControllableState(\n      singleProps.value,\n      singleProps.defaultValue ?? min,\n      singleProps.onValueChange,\n    );\n\n    const slider = !showControl ? (\n      <DisplayValue\n        size={size}\n        className={className}\n        interactive={interactive}\n        onActivate={enterEdit}\n      >\n        {`${prefix ?? \"\"}${value}${suffix ?? \"\"}`}\n      <\/DisplayValue>\n    ) : (\n      <div data-react-fancy-slider=\"\" className={cn(\"flex flex-col gap-1\", className)}>\n        <div className=\"flex items-center gap-3\">\n          <input\n            ref={ref}\n            id={id}\n            type=\"range\"\n            name={name}\n            min={min}\n            max={max}\n            step={step}\n            value={value}\n            disabled={disabled}\n            autoFocus={interactive}\n            onBlur={() => {\n              if (interactive) exitEdit();\n            }}\n            onChange={(e) => setValue(Number(e.target.value))}\n            className={cn(\n              \"w-full cursor-pointer accent-blue-600 disabled:cursor-not-allowed disabled:opacity-50\",\n              dirtyRingClasses(dirty),\n              {\n                xs: \"h-1\",\n                sm: \"h-1.5\",\n                md: \"h-2\",\n                lg: \"h-2.5\",\n                xl: \"h-3\",\n              }[size],\n            )}\n          \/>\n          {showValue && (\n            <span className=\"min-w-[3ch] shrink-0 whitespace-nowrap text-right text-sm font-medium text-[color:var(--fancy-text-soft,var(--color-zinc-700))] dark:text-[color:var(--fancy-text-soft-dark,var(--color-zinc-300))]\">\n              {prefix}{value}{suffix}\n            <\/span>\n          )}\n        <\/div>\n        {marks && marks.length > 0 && <SliderMarks marks={marks} min={min} max={max} \/>}\n      <\/div>\n    );\n\n    if (label || error || description) {\n      return (\n        <Field label={label} labelHidden={labelHidden} description={description} error={error} required={required} htmlFor={id} size={size}>\n          {slider}\n        <\/Field>\n      );\n    }\n\n    return slider;\n  },\n);\n\nSingleSlider.displayName = \"SingleSlider\";\n\nconst RangeSlider = forwardRef<HTMLInputElement, SliderProps & { range: true }>(\n  (\n    {\n      size = \"md\",\n      dirty,\n      error,\n      label,\n      labelHidden,\n      description,\n      required,\n      disabled,\n      className,\n      id,\n      name,\n      min = 0,\n      max = 100,\n      step = 1,\n      showValue,\n      marks,\n      prefix,\n      suffix,\n      mode,\n      ...rest\n    },\n    ref,\n  ) => {\n    const rangeProps = rest as { value?: [number, number]; defaultValue?: [number, number]; onValueChange?: (v: [number, number]) => void };\n    const resolvedMode = useFieldMode(mode);\n    const { showControl, interactive, enterEdit, exitEdit } = useInlineEdit(resolvedMode, disabled);\n    const [value, setValue] = useControllableState(\n      rangeProps.value,\n      rangeProps.defaultValue ?? [min, max] as [number, number],\n      rangeProps.onValueChange,\n    );\n\n    const handleMin = (n: number) => {\n      setValue([Math.min(n, value[1]), value[1]]);\n    };\n\n    const handleMax = (n: number) => {\n      setValue([value[0], Math.max(n, value[0])]);\n    };\n\n    const leftPercent = ((value[0] - min) \/ (max - min)) * 100;\n    const rightPercent = ((value[1] - min) \/ (max - min)) * 100;\n\n    const slider = !showControl ? (\n      <DisplayValue\n        size={size}\n        className={className}\n        interactive={interactive}\n        onActivate={enterEdit}\n      >\n        {`${prefix ?? \"\"}${value[0]}${suffix ?? \"\"}\u2013${prefix ?? \"\"}${value[1]}${suffix ?? \"\"}`}\n      <\/DisplayValue>\n    ) : (\n      <div\n        data-react-fancy-slider=\"\"\n        className={cn(\"flex flex-col gap-1\", className)}\n        onBlur={(e) => {\n          if (interactive && !e.currentTarget.contains(e.relatedTarget as Node)) exitEdit();\n        }}\n      >\n        <div className=\"flex items-center gap-3\">\n          <div className=\"relative w-full\">\n            <div className=\"pointer-events-none absolute top-1\/2 h-1.5 w-full -translate-y-1\/2 rounded-full bg-[color:var(--fancy-border,var(--color-zinc-200))] dark:bg-[color:var(--fancy-border-dark,var(--color-zinc-700))]\" \/>\n            <div\n              className=\"pointer-events-none absolute top-1\/2 h-1.5 -translate-y-1\/2 rounded-full bg-blue-500\"\n              style={{ left: `${leftPercent}%`, width: `${rightPercent - leftPercent}%` }}\n            \/>\n            <input\n              ref={ref}\n              type=\"range\"\n              min={min}\n              max={max}\n              step={step}\n              value={value[0]}\n              disabled={disabled}\n              autoFocus={interactive}\n              onChange={(e) => handleMin(Number(e.target.value))}\n              className={cn(\n                \"pointer-events-none absolute w-full cursor-pointer appearance-none bg-transparent [&::-webkit-slider-thumb]:pointer-events-auto [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-blue-600 [&::-webkit-slider-thumb]:shadow\",\n                dirtyRingClasses(dirty),\n              )}\n            \/>\n            <input\n              type=\"range\"\n              min={min}\n              max={max}\n              step={step}\n              value={value[1]}\n              disabled={disabled}\n              onChange={(e) => handleMax(Number(e.target.value))}\n              className=\"pointer-events-none absolute w-full cursor-pointer appearance-none bg-transparent [&::-webkit-slider-thumb]:pointer-events-auto [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-blue-600 [&::-webkit-slider-thumb]:shadow\"\n            \/>\n            <div className=\"h-6\" \/>\n          <\/div>\n          {showValue && (\n            <span className=\"min-w-[6ch] shrink-0 whitespace-nowrap text-right text-sm font-medium text-[color:var(--fancy-text-soft,var(--color-zinc-700))] dark:text-[color:var(--fancy-text-soft-dark,var(--color-zinc-300))]\">\n              {prefix}{value[0]}{suffix}\u2013{prefix}{value[1]}{suffix}\n            <\/span>\n          )}\n        <\/div>\n        {marks && marks.length > 0 && <SliderMarks marks={marks} min={min} max={max} \/>}\n      <\/div>\n    );\n\n    if (label || error || description) {\n      return (\n        <Field label={label} labelHidden={labelHidden} description={description} error={error} required={required} htmlFor={id} size={size}>\n          {slider}\n        <\/Field>\n      );\n    }\n\n    return slider;\n  },\n);\n\nRangeSlider.displayName = \"RangeSlider\";\n\nfunction SliderMarks({\n  marks,\n  min,\n  max,\n}: {\n  marks: { value: number; label?: string }[];\n  min: number;\n  max: number;\n}) {\n  return (\n    <div className=\"relative h-5 w-full\">\n      {marks.map((mark) => {\n        const percent = ((mark.value - min) \/ (max - min)) * 100;\n        return (\n          <span\n            key={mark.value}\n            className=\"absolute -translate-x-1\/2 text-xs text-[color:var(--fancy-text-muted,var(--color-zinc-500))] dark:text-[color:var(--fancy-text-muted-dark,var(--color-zinc-400))]\"\n            style={{ left: `${percent}%` }}\n          >\n            {mark.label ?? mark.value}\n          <\/span>\n        );\n      })}\n    <\/div>\n  );\n}\n","type":"registry:ui","target":"components\/fancy\/slider\/Slider.tsx"},{"path":"components\/fancy\/slider\/Slider.types.ts","content":"import type { ReactNode } from \"react\";\nimport type { InputBaseProps } from \"..\/lib\/components\/inputs\/inputs.types\";\n\ninterface SliderBaseProps extends InputBaseProps {\n  min?: number;\n  max?: number;\n  step?: number;\n  showValue?: boolean;\n  marks?: { value: number; label?: string }[];\n  prefix?: ReactNode;\n  suffix?: ReactNode;\n}\n\nexport interface SliderSingleProps extends SliderBaseProps {\n  range?: false;\n  value?: number;\n  defaultValue?: number;\n  onValueChange?: (value: number) => void;\n}\n\nexport interface SliderRangeProps extends SliderBaseProps {\n  range: true;\n  value?: [number, number];\n  defaultValue?: [number, number];\n  onValueChange?: (value: [number, number]) => void;\n}\n\nexport type SliderProps = SliderSingleProps | SliderRangeProps;\n","type":"registry:ui","target":"components\/fancy\/slider\/Slider.types.ts"},{"path":"components\/fancy\/slider\/index.ts","content":"export { Slider } from \".\/Slider\";\nexport type { SliderProps, SliderSingleProps, SliderRangeProps } from \".\/Slider.types\";\n","type":"registry:ui","target":"components\/fancy\/slider\/index.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\/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"},{"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"}]}