{"$schema":"https:\/\/ui.particle.academy\/schema\/registry-item.json","name":"input","type":"registry:ui","title":"Input","description":"Text input with labels, validation, affixes and controlled value helpers.","package":"react-fancy","dependencies":["tailwind-merge"],"registryDependencies":["field","paste-pills","popover","portal"],"files":[{"path":"components\/fancy\/input\/Input.tsx","content":"import { forwardRef, useId, useMemo, useRef } from \"react\";\nimport { cn } from \"..\/lib\/utils\/cn\";\nimport { useControllableState } from \"..\/lib\/hooks\/use-controllable-state\";\nimport { useAdornmentWidth } from \"..\/lib\/hooks\/use-adornment-width\";\nimport { Field } from \"..\/field\";\nimport { InputWrapper } from \"..\/lib\/components\/inputs\/InputWrapper\";\nimport {\n  dirtyClasses,\n  errorClasses,\n  inputBaseClasses,\n  inputSizeClasses,\n} from \"..\/lib\/components\/inputs\/inputs.utils\";\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 {\n  InlinePasteEditor,\n  PastedFilesInput,\n  useInlinePaste,\n  type InlinePasteEditorHandle,\n} from \"..\/paste-pills\";\nimport type { InputProps } from \".\/Input.types\";\n\nexport const Input = forwardRef<HTMLInputElement, InputProps>(\n  (\n    {\n      type = \"text\",\n      size = \"md\",\n      dirty,\n      error,\n      label,\n      labelHidden,\n      description,\n      required,\n      disabled,\n      className,\n      id,\n      leading,\n      trailing,\n      prefix,\n      suffix,\n      prefixPosition,\n      suffixPosition,\n      mode,\n      onValueChange,\n      onChange,\n      reveal,\n      revealed,\n      onRevealedChange,\n      revealLabel = \"Show password\",\n      hideLabel = \"Hide password\",\n      pasteThreshold,\n      pasteImages = false,\n      pasteFilesName,\n      previewChars = 20,\n      renderPastePreview,\n      pasteClassNames,\n      heldPastes,\n      onHeldPastesChange,\n      heldImages,\n      onHeldImagesChange,\n      ...props\n    },\n    ref,\n  ) => {\n    const autoId = useId();\n    const inputId = id ?? autoId;\n    const resolvedMode = useFieldMode(mode);\n    const { showControl, interactive, enterEdit, exitEdit } = useInlineEdit(resolvedMode, disabled);\n\n    \/\/ Measured, not assumed: `leading`\/`trailing` take arbitrary content, and a\n    \/\/ fixed padding can only ever be right for one width. See\n    \/\/ `useAdornmentWidth` for what went wrong with the fixed version.\n    const [leadingRef, leadingWidth] = useAdornmentWidth();\n    const [trailingRef, trailingWidth] = useAdornmentWidth();\n\n    \/\/ Only meaningful on a password field: a reveal on an already-visible input\n    \/\/ is a button that does nothing.\n    const canReveal = reveal === true && type === \"password\";\n    const [isRevealed, setRevealed] = useControllableState(revealed, false, onRevealedChange);\n    const shown = canReveal && isRevealed;\n    \/\/ The rendered type, NOT the declared one \u2014 `type` stays \"password\" so the\n    \/\/ display-mode masking below and any caller reading props are unaffected.\n    const renderedType = shown ? \"text\" : type;\n\n    \/*\n     * Paste pills: the same core `Composer` and `PromptInput` use, so the three\n     * cannot disagree about what a pill holds or what a send carries. Hooks run\n     * whether or not it is on (Rules of Hooks); nothing is rendered unless asked.\n     *\/\n    const pasting = pasteThreshold !== undefined || pasteImages;\n    const editorRef = useRef<InlinePasteEditorHandle>(null);\n    const hiddenRef = useRef<HTMLInputElement | null>(null);\n    const paste = useInlinePaste({\n      pasteThreshold,\n      pasteImages,\n      previewChars,\n      renderPastePreview,\n      classNames: pasteClassNames,\n      disabled: disabled === true,\n      heldPastes,\n      onHeldPastesChange,\n      heldImages,\n      onHeldImagesChange,\n      editorRef,\n      blockText: false,\n    });\n    const [pasteValue, setPasteValue] = useControllableState<string>(\n      props.value === undefined ? undefined : String(props.value),\n      props.defaultValue === undefined ? \"\" : String(props.defaultValue),\n      onValueChange,\n    );\n    const filesName = pasteFilesName ?? (props.name ? `${props.name}_files[]` : undefined);\n    const pastedFiles = useMemo(\n      () => (pasteImages ? paste.imagesIn(pasteValue).map((i) => i.file) : []),\n      [pasteImages, paste, pasteValue],\n    );\n    const labelId = `${inputId}-label`;\n\n    const input = !showControl ? (\n      <DisplayValue\n        size={size}\n        interactive={interactive}\n        onActivate={enterEdit}\n        leading={leading ?? prefix}\n        trailing={trailing ?? suffix}\n      >\n        {type === \"password\" ? (props.value ? \"\u2022\u2022\u2022\u2022\u2022\u2022\" : \"\") : (props.value as string | undefined)}\n      <\/DisplayValue>\n    ) : (\n      <InputWrapper\n        prefix={prefix}\n        suffix={suffix}\n        prefixPosition={prefixPosition}\n        suffixPosition={suffixPosition}\n        size={size}\n      >\n        <div data-react-fancy-input=\"\" className=\"relative flex items-center\">\n          {leading && (\n            <span\n              ref={leadingRef}\n              className=\"pointer-events-none absolute left-3 text-[color:var(--fancy-text-faint,var(--color-zinc-400))] dark:text-[color:var(--fancy-text-faint-dark,var(--color-zinc-500))]\"\n            >\n              {leading}\n            <\/span>\n          )}\n          {pasting ? (\n            <>\n              <div className=\"min-w-0 flex-1\">\n                <InlinePasteEditor\n                  ref={editorRef}\n                  id={inputId}\n                  value={pasteValue}\n                  known={paste.known}\n                  onChange={(next) => {\n                    setPasteValue(next);\n                    paste.prune(next);\n                  }}\n                  \/\/ Enter in a text field submits its form; keep that.\n                  onSubmit={() => hiddenRef.current?.form?.requestSubmit()}\n                  multiline={false}\n                  onPaste={paste.decidePaste}\n                  onHostsChange={paste.onHostsChange}\n                  placeholder={props.placeholder ?? \"\"}\n                  disabled={disabled === true}\n                  ariaLabelledBy={label ? labelId : props[\"aria-labelledby\"]}\n                  ariaInvalid={Boolean(error)}\n                  placeholderClassName={cn(pastePlaceholderClasses[size], leading ? \"left-9\" : undefined)}\n                  style={{\n                    ...(leadingWidth ? { paddingLeft: leadingWidth + 20 } : null),\n                    ...(trailingWidth ? { paddingRight: trailingWidth + 20 } : null),\n                  }}\n                  className={cn(\n                    inputBaseClasses,\n                    inputSizeClasses[size],\n                    dirtyClasses(dirty),\n                    errorClasses(error),\n                    \/\/ One line, like the <input> it stands in for.\n                    \"min-h-0 max-h-none w-full overflow-x-auto overflow-y-hidden whitespace-nowrap break-normal\",\n                    leading && !leadingWidth && \"pl-9\",\n                    trailing && !trailingWidth && \"pr-9\",\n                    className,\n                    pasteClassNames?.input,\n                  )}\n                \/>\n              <\/div>\n              {paste.portals}\n              {\/* What a native submit posts: the value with every pill expanded. *\/}\n              <input\n                ref={(el) => {\n                  hiddenRef.current = el;\n                  if (typeof ref === \"function\") ref(el);\n                  else if (ref) ref.current = el;\n                }}\n                type=\"hidden\"\n                name={props.name}\n                form={props.form}\n                value={paste.expand(pasteValue)}\n              \/>\n              {pasteImages && filesName && (\n                <PastedFilesInput name={filesName} files={pastedFiles} form={props.form} \/>\n              )}\n            <\/>\n          ) : (\n            <input\n              ref={ref}\n              id={inputId}\n              type={renderedType}\n              disabled={disabled}\n              required={required}\n              className={cn(\n                inputBaseClasses,\n                inputSizeClasses[size],\n                dirtyClasses(dirty),\n                errorClasses(error),\n                \"w-full\",\n                \/\/ The fixed classes are the PRE-MEASURE fallback only. Once the\n                \/\/ adornment is measured, the inline style below overrides them\n                \/\/ with its real width -- a fixed 36px is correct for one icon and\n                \/\/ wrong for anything wider, which is how `https:\/\/` came to\n                \/\/ render on top of the value.\n                leading && !leadingWidth && \"pl-9\",\n                (trailing || canReveal) && !trailingWidth && \"pr-9\",\n                className,\n              )}\n              style={{\n                \/\/ 12px clears `left-3`\/`right-3`; 8px is the gap to the text.\n                ...(leadingWidth ? { paddingLeft: leadingWidth + 20 } : null),\n                ...(trailingWidth ? { paddingRight: trailingWidth + 20 } : null),\n                ...props.style,\n              }}\n              onChange={(e) => {\n                onChange?.(e);\n                onValueChange?.(e.target.value);\n              }}\n              {...props}\n              \/\/ Inline click-to-edit: focus on entering edit, return to the display\n              \/\/ (and run the host's onBlur) when focus leaves.\n              autoFocus={interactive || props.autoFocus}\n              onBlur={(e) => {\n                props.onBlur?.(e);\n                if (interactive) exitEdit();\n              }}\n            \/>\n          )}\n          {canReveal ? (\n            <button\n              type=\"button\"\n              \/\/ type=\"button\" is load-bearing: the default is \"submit\", so a\n              \/\/ reveal inside a login form would submit it on every click.\n              id={`${inputId}-reveal`}\n              data-react-fancy-reveal=\"\"\n              onClick={() => setRevealed((v) => !v)}\n              disabled={disabled}\n              aria-controls={inputId}\n              aria-pressed={shown}\n              aria-label={shown ? hideLabel : revealLabel}\n              title={shown ? hideLabel : revealLabel}\n              \/\/ Not focusable by tab: the field and the submit button are the\n              \/\/ path through a login form, and a reveal in between is a stop\n              \/\/ most people do not want. Still reachable by click and by\n              \/\/ screen readers.\n              tabIndex={-1}\n              className=\"absolute right-2 inline-flex items-center justify-center rounded p-1 text-[color:var(--fancy-text-faint,var(--color-zinc-400))] transition-colors hover:text-zinc-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500\/40 disabled:cursor-not-allowed disabled:opacity-50 dark:text-[color:var(--fancy-text-faint-dark,var(--color-zinc-500))] dark:hover:text-zinc-300\"\n            >\n              {shown ? <EyeOffIcon \/> : <EyeIcon \/>}\n            <\/button>\n          ) : (\n            trailing && (\n              <span\n                ref={trailingRef}\n                className=\"pointer-events-none absolute right-3 text-[color:var(--fancy-text-faint,var(--color-zinc-400))] dark:text-[color:var(--fancy-text-faint-dark,var(--color-zinc-500))]\"\n              >\n                {trailing}\n              <\/span>\n            )\n          )}\n        <\/div>\n      <\/InputWrapper>\n    );\n\n    if (label || error || description) {\n      return (\n        <Field\n          label={label}\n          labelId={pasting ? labelId : undefined}\n          labelHidden={labelHidden}\n          description={description}\n          error={error}\n          required={required}\n          htmlFor={inputId}\n          size={size}\n        >\n          {input}\n        <\/Field>\n      );\n    }\n\n    return input;\n  },\n);\n\nInput.displayName = \"Input\";\n\n\/** Where the placeholder sits over the paste editor, matching `inputSizeClasses`. *\/\nconst pastePlaceholderClasses = {\n  xs: \"left-2 top-1 text-xs\",\n  sm: \"left-2.5 top-1.5 text-sm\",\n  md: \"left-3 top-2 text-sm\",\n  lg: \"left-4 top-2.5 text-base\",\n  xl: \"left-5 top-3 text-lg\",\n} as const;\n\n\/**\n * Inline SVGs rather than the Icon component: Input is the most-installed\n * primitive in the kit, and routing it through the icon registry would make\n * every consumer's password field depend on an icon set being configured.\n *\/\nfunction EyeIcon() {\n  return (\n    <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n      <path d=\"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0\" \/>\n      <circle cx=\"12\" cy=\"12\" r=\"3\" \/>\n    <\/svg>\n  );\n}\n\nfunction EyeOffIcon() {\n  return (\n    <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n      <path d=\"M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49\" \/>\n      <path d=\"M14.084 14.158a3 3 0 0 1-4.242-4.242\" \/>\n      <path d=\"M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143\" \/>\n      <path d=\"m2 2 20 20\" \/>\n    <\/svg>\n  );\n}\n","type":"registry:ui","target":"components\/fancy\/input\/Input.tsx"},{"path":"components\/fancy\/input\/Input.types.ts","content":"import type { InputHTMLAttributes, ReactNode } from \"react\";\nimport type { InputBaseProps, InputAffixProps } from \"..\/lib\/components\/inputs\/inputs.types\";\nimport type {\n  ComposerPasteClassNames,\n  ComposerPastePreview,\n  HeldImage,\n  HeldPaste,\n} from \"..\/paste-pills\";\n\nexport interface InputProps\n  extends InputBaseProps,\n    InputAffixProps,\n    Omit<InputHTMLAttributes<HTMLInputElement>, \"size\" | \"type\" | \"prefix\"> {\n  type?: \"text\" | \"email\" | \"password\" | \"number\" | \"tel\" | \"url\" | \"search\";\n  onValueChange?: (value: string) => void;\n  leading?: ReactNode;\n  trailing?: ReactNode;\n  \/**\n   * Show a toggle that reveals the value, for `type=\"password\"`.\n   *\n   * Ignored on other input types \u2014 a reveal on a field that is already visible\n   * is a button that does nothing.\n   *\n   * It occupies the trailing slot, so it replaces `trailing` when both are set:\n   * the two would otherwise stack in the same corner, and the reveal is the one\n   * a person needs to click.\n   *\/\n  reveal?: boolean;\n  \/** Controlled reveal state. Omit to let the input own it. *\/\n  revealed?: boolean;\n  onRevealedChange?: (revealed: boolean) => void;\n  \/** Accessible label for the toggle. Defaults to \"Show password\" \/ \"Hide password\". *\/\n  revealLabel?: string;\n  hideLabel?: string;\n  \/**\n   * Hold a paste of at least this many characters as a pill in the field, as\n   * `Composer` and `PromptInput` do \u2014 same code, from `PastePills`. Try 500.\n   *\n   * **Omit it (and `pasteImages`) and this is a plain `<input>`, exactly as\n   * before.** Setting either swaps it for a single-line editor that can hold a\n   * pill, plus a hidden `<input name={name}>` that posts the value with every pill\n   * expanded, so a native form submit sends the whole paste. `ref` then points at\n   * that hidden input; `onValueChange` receives the value with a marker for each\n   * pill (see `pasteMarker`), and `onChange` is not called.\n   *\/\n  pasteThreshold?: number;\n  \/**\n   * Hold a pasted image as a pill, and put the file in a hidden\n   * `<input type=\"file\" multiple>` in the same form so a submit posts it. The text\n   * marks where it sat as `[Pasted image N]`.\n   *\/\n  pasteImages?: boolean;\n  \/**\n   * The field name pasted files are posted under. Defaults to `{name}_files[]`\n   * (an array for PHP \/ Laravel); without either, the files are held but not\n   * posted.\n   *\/\n  pasteFilesName?: string;\n  \/** Characters of a held paste shown at EACH end of its hover preview. Default 20. *\/\n  previewChars?: number;\n  renderPastePreview?: (preview: ComposerPastePreview) => ReactNode;\n  pasteClassNames?: ComposerPasteClassNames;\n  \/** The held pastes and images, if you want to own them (an agent attaching one, a restored draft). *\/\n  heldPastes?: HeldPaste[];\n  onHeldPastesChange?: (pastes: HeldPaste[]) => void;\n  heldImages?: HeldImage[];\n  onHeldImagesChange?: (images: HeldImage[]) => void;\n}\n","type":"registry:ui","target":"components\/fancy\/input\/Input.types.ts"},{"path":"components\/fancy\/input\/index.ts","content":"export { Input } from \".\/Input\";\nexport type { InputProps } from \".\/Input.types\";\n","type":"registry:ui","target":"components\/fancy\/input\/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\/hooks\/use-adornment-width.ts","content":"import { useCallback, useEffect, useState } from \"react\";\n\n\/**\n * Measure an inline adornment so the input can reserve exactly its width.\n *\n * ## Why this exists\n *\n * `leading` \/ `trailing` render ABSOLUTELY over the input, so they take no\n * space in flow and the input must be padded by hand to keep text clear of\n * them. That padding used to be a fixed `pl-9` \/ `pr-9` \u2014 36px, sized for a\n * single icon. Any wider adornment ran straight under the value: `leading =\n * \"https:\/\/\"` is roughly 50px, so `https:\/\/` and `fancy.app` rendered on top of\n * one another.\n *\n * A fixed padding cannot be right for arbitrary content, and these props accept\n * arbitrary content \u2014 a currency code, a unit, a protocol, a short label. So\n * the width is measured rather than assumed.\n *\n * Returns `0` until measured (and on the server), which is the signal to keep\n * the old fixed padding as a fallback: an unmeasured render is then no worse\n * than it was before, rather than unpadded.\n *\n * `ResizeObserver` rather than a one-shot read, because the width changes after\n * first paint for reasons the first paint cannot see \u2014 a webfont swapping in,\n * the adornment's own content changing, a container query resizing the text.\n *\/\nexport function useAdornmentWidth(): [(node: HTMLElement | null) => void, number] {\n  const [width, setWidth] = useState(0);\n  const [node, setNode] = useState<HTMLElement | null>(null);\n\n  const ref = useCallback((n: HTMLElement | null) => setNode(n), []);\n\n  useEffect(() => {\n    if (!node) {\n      setWidth(0);\n\n      return;\n    }\n\n    const read = () => setWidth(node.getBoundingClientRect().width);\n    read();\n\n    if (typeof ResizeObserver === \"undefined\") return;\n\n    const observer = new ResizeObserver(read);\n    observer.observe(node);\n\n    return () => observer.disconnect();\n  }, [node]);\n\n  return [ref, width];\n}\n","type":"registry:lib","target":"components\/fancy\/lib\/hooks\/use-adornment-width.ts"},{"path":"components\/fancy\/lib\/components\/inputs\/InputWrapper.tsx","content":"import type { ReactNode } from \"react\";\nimport { cn } from \"..\/..\/utils\/cn\";\nimport type { AffixPosition } from \".\/inputs.types\";\nimport type { Size } from \"..\/..\/utils\/types\";\n\ninterface InputWrapperProps {\n  children: ReactNode;\n  prefix?: ReactNode;\n  suffix?: ReactNode;\n  prefixPosition?: AffixPosition;\n  suffixPosition?: AffixPosition;\n  size?: Size;\n  \/** Extra padding className to apply to the input when affixes are \"inside\" *\/\n  insidePrefixPadding?: string;\n  insideSuffixPadding?: string;\n}\n\n\/\/ NOTE: these must be COMPLETE, static class strings. Tailwind only detects\n\/\/ utilities it can see verbatim in the source \u2014 building the arbitrary variant\n\/\/ at runtime (e.g. `[&_input]:${\"pl-9\"}`) means `[&_input]:pl-9` is never\n\/\/ emitted to CSS, so the inside affix gets no padding and overlaps the text\n\/\/ (issue #6). Keep input\/select\/textarea spelled out here.\nconst insidePaddingLeft: Record<Size, string> = {\n  xs: \"[&_input]:pl-7 [&_select]:pl-7 [&_textarea]:pl-7\",\n  sm: \"[&_input]:pl-8 [&_select]:pl-8 [&_textarea]:pl-8\",\n  md: \"[&_input]:pl-9 [&_select]:pl-9 [&_textarea]:pl-9\",\n  lg: \"[&_input]:pl-10 [&_select]:pl-10 [&_textarea]:pl-10\",\n  xl: \"[&_input]:pl-11 [&_select]:pl-11 [&_textarea]:pl-11\",\n};\n\nconst insidePaddingRight: Record<Size, string> = {\n  xs: \"[&_input]:pr-7 [&_select]:pr-7 [&_textarea]:pr-7\",\n  sm: \"[&_input]:pr-8 [&_select]:pr-8 [&_textarea]:pr-8\",\n  md: \"[&_input]:pr-9 [&_select]:pr-9 [&_textarea]:pr-9\",\n  lg: \"[&_input]:pr-10 [&_select]:pr-10 [&_textarea]:pr-10\",\n  xl: \"[&_input]:pr-11 [&_select]:pr-11 [&_textarea]:pr-11\",\n};\n\nconst affixOutsideClasses =\n  \"inline-flex items-center border border-[color:var(--fancy-border-strong,var(--color-zinc-300))] bg-zinc-50 px-3 text-sm text-[color:var(--fancy-text-muted,var(--color-zinc-500))] dark:border-[color:var(--fancy-border-strong-dark,var(--color-zinc-700))] dark:bg-zinc-800 dark:text-[color:var(--fancy-text-muted-dark,var(--color-zinc-400))]\";\n\nexport function InputWrapper({\n  children,\n  prefix,\n  suffix,\n  prefixPosition = \"inside\",\n  suffixPosition = \"inside\",\n  size = \"md\",\n}: InputWrapperProps) {\n  const hasInsidePrefix = prefix && prefixPosition === \"inside\";\n  const hasInsideSuffix = suffix && suffixPosition === \"inside\";\n  const hasOutsidePrefix = prefix && prefixPosition === \"outside\";\n  const hasOutsideSuffix = suffix && suffixPosition === \"outside\";\n  const hasOutside = hasOutsidePrefix || hasOutsideSuffix;\n\n  if (!prefix && !suffix) {\n    return <>{children}<\/>;\n  }\n\n  if (hasOutside) {\n    return (\n      <div data-react-fancy-input-wrapper=\"\" className=\"flex\">\n        {hasOutsidePrefix && (\n          <span className={cn(affixOutsideClasses, \"rounded-l-lg border-r-0\")}>\n            {prefix}\n          <\/span>\n        )}\n        <div\n          className={cn(\n            \"relative flex flex-1 items-center\",\n            hasInsidePrefix && \"relative\",\n          )}\n        >\n          {hasInsidePrefix && (\n            <span className=\"pointer-events-none absolute left-3 z-10 text-[color:var(--fancy-text-faint,var(--color-zinc-400))] dark:text-[color:var(--fancy-text-faint-dark,var(--color-zinc-500))]\">\n              {prefix}\n            <\/span>\n          )}\n          <div\n            className={cn(\n              \"w-full [&>*]:w-full\",\n              hasOutsidePrefix && !hasOutsideSuffix && \"[&_input]:rounded-l-none [&_select]:rounded-l-none [&_textarea]:rounded-l-none\",\n              !hasOutsidePrefix && hasOutsideSuffix && \"[&_input]:rounded-r-none [&_select]:rounded-r-none [&_textarea]:rounded-r-none\",\n              hasOutsidePrefix && hasOutsideSuffix && \"[&_input]:rounded-none [&_select]:rounded-none [&_textarea]:rounded-none\",\n              hasInsidePrefix && insidePaddingLeft[size],\n            )}\n          >\n            {children}\n          <\/div>\n          {hasInsideSuffix && (\n            <span className=\"pointer-events-none absolute right-3 z-10 text-[color:var(--fancy-text-faint,var(--color-zinc-400))] dark:text-[color:var(--fancy-text-faint-dark,var(--color-zinc-500))]\">\n              {suffix}\n            <\/span>\n          )}\n        <\/div>\n        {hasOutsideSuffix && (\n          <span className={cn(affixOutsideClasses, \"rounded-r-lg border-l-0\")}>\n            {suffix}\n          <\/span>\n        )}\n      <\/div>\n    );\n  }\n\n  \/\/ Both inside\n  return (\n    <div data-react-fancy-input-wrapper=\"\" className=\"relative flex items-center\">\n      {hasInsidePrefix && (\n        <span className=\"pointer-events-none absolute left-3 z-10 text-[color:var(--fancy-text-faint,var(--color-zinc-400))] dark:text-[color:var(--fancy-text-faint-dark,var(--color-zinc-500))]\">\n          {prefix}\n        <\/span>\n      )}\n      <div\n        className={cn(\n          \"w-full\",\n          hasInsidePrefix && insidePaddingLeft[size],\n          hasInsideSuffix && insidePaddingRight[size],\n        )}\n      >\n        {children}\n      <\/div>\n      {hasInsideSuffix && (\n        <span className=\"pointer-events-none absolute right-3 z-10 text-[color:var(--fancy-text-faint,var(--color-zinc-400))] dark:text-[color:var(--fancy-text-faint-dark,var(--color-zinc-500))]\">\n          {suffix}\n        <\/span>\n      )}\n    <\/div>\n  );\n}\n","type":"registry:lib","target":"components\/fancy\/lib\/components\/inputs\/InputWrapper.tsx"},{"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\/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.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"},{"path":"components\/fancy\/lib\/hooks\/use-floating-position.ts","content":"import { useCallback, useEffect, useState, type RefObject } from \"react\";\nimport type { Placement } from \"..\/utils\/types\";\n\ninterface FloatingPosition {\n  x: number;\n  y: number;\n  placement: Placement;\n}\n\ninterface UseFloatingPositionOptions {\n  placement?: Placement;\n  offset?: number;\n  enabled?: boolean;\n}\n\nfunction getPosition(\n  anchor: DOMRect,\n  floating: DOMRect,\n  placement: Placement,\n  offset: number,\n): FloatingPosition {\n  let x = 0;\n  let y = 0;\n  const base = placement.split(\"-\")[0] as \"top\" | \"bottom\" | \"left\" | \"right\";\n  const align = placement.split(\"-\")[1] as \"start\" | \"end\" | undefined;\n\n  switch (base) {\n    case \"top\":\n      x = anchor.left + anchor.width \/ 2 - floating.width \/ 2;\n      y = anchor.top - floating.height - offset;\n      break;\n    case \"bottom\":\n      x = anchor.left + anchor.width \/ 2 - floating.width \/ 2;\n      y = anchor.bottom + offset;\n      break;\n    case \"left\":\n      x = anchor.left - floating.width - offset;\n      y = anchor.top + anchor.height \/ 2 - floating.height \/ 2;\n      break;\n    case \"right\":\n      x = anchor.right + offset;\n      y = anchor.top + anchor.height \/ 2 - floating.height \/ 2;\n      break;\n  }\n\n  if (base === \"top\" || base === \"bottom\") {\n    if (align === \"start\") x = anchor.left;\n    else if (align === \"end\") x = anchor.right - floating.width;\n  }\n\n  if (base === \"left\" || base === \"right\") {\n    if (align === \"start\") y = anchor.top;\n    else if (align === \"end\") y = anchor.bottom - floating.height;\n  }\n\n  \/\/ Viewport clamping\n  let finalPlacement = placement;\n  const vw = window.innerWidth;\n  const vh = window.innerHeight;\n\n  if (base === \"bottom\" && y + floating.height > vh) {\n    y = anchor.top - floating.height - offset;\n    finalPlacement = placement.replace(\"bottom\", \"top\") as Placement;\n  } else if (base === \"top\" && y < 0) {\n    y = anchor.bottom + offset;\n    finalPlacement = placement.replace(\"top\", \"bottom\") as Placement;\n  }\n\n  x = Math.max(4, Math.min(x, vw - floating.width - 4));\n  y = Math.max(4, Math.min(y, vh - floating.height - 4));\n\n  return { x, y, placement: finalPlacement };\n}\n\nexport function useFloatingPosition(\n  anchorRef: RefObject<HTMLElement | null>,\n  floatingRef: RefObject<HTMLElement | null>,\n  options: UseFloatingPositionOptions = {},\n): FloatingPosition {\n  const { placement = \"bottom\", offset = 8, enabled = true } = options;\n  const [position, setPosition] = useState<FloatingPosition>({\n    x: -9999,\n    y: -9999,\n    placement,\n  });\n\n  const update = useCallback(() => {\n    const anchor = anchorRef.current;\n    const floating = floatingRef.current;\n    if (!anchor || !floating) return;\n\n    const anchorRect = anchor.getBoundingClientRect();\n    const floatingRect = floating.getBoundingClientRect();\n    const next = getPosition(anchorRect, floatingRect, placement, offset);\n    \/\/ Bail out when nothing changed so this can run after every commit\n    \/\/ (below) without causing an infinite render loop.\n    setPosition((prev) =>\n      prev.x === next.x && prev.y === next.y && prev.placement === next.placement\n        ? prev\n        : next,\n    );\n  }, [anchorRef, floatingRef, placement, offset]);\n\n  \/\/ Re-measure after every commit while open.\n  \/\/\n  \/\/ The floating element mounts a frame or two AFTER `enabled` flips \u2014 its\n  \/\/ render is gated behind a state flip (e.g. useAnimation's `mounted`), so at\n  \/\/ the moment `enabled` turns true the floating ref is still null and a single\n  \/\/ measurement no-ops, leaving the popover parked at its off-screen sentinel.\n  \/\/\n  \/\/ A passive effect with no deps runs after *every* commit \u2014 including the one\n  \/\/ that finally mounts the floating element \u2014 and, crucially, keeps running in\n  \/\/ hidden\/background tabs where `requestAnimationFrame` is paused. The bail-out\n  \/\/ in `update` makes the steady-state calls free, so this both fixes the\n  \/\/ late-mount race and survives tab-visibility throttling.\n  useEffect(() => {\n    if (enabled) update();\n  });\n\n  \/\/ \u2026but the per-commit effect ISN'T enough when the floating element mounts\n  \/\/ through a <Portal> whose own mount gate (SSR-safe; renders nothing until its\n  \/\/ useEffect runs) attaches the node WITHOUT re-rendering this hook's component.\n  \/\/ Then no commit happens here, the measurement never runs, and the popover\n  \/\/ stays parked at its off-screen sentinel until an unrelated scroll\/resize\n  \/\/ fires `update()` \u2014 the \"menu only appears after I scroll\" bug. Poll with rAF\n  \/\/ until the node is actually in the DOM, measure it, then keep it observed for\n  \/\/ size changes (async content \/ lazily-loaded icons).\n  useEffect(() => {\n    if (!enabled) return;\n    let raf = 0;\n    let ro: ResizeObserver | null = null;\n    const tick = () => {\n      const el = floatingRef.current;\n      if (el) {\n        update();\n        if (typeof ResizeObserver !== \"undefined\") {\n          ro = new ResizeObserver(() => update());\n          ro.observe(el);\n        }\n      } else {\n        raf = requestAnimationFrame(tick);\n      }\n    };\n    raf = requestAnimationFrame(tick);\n    return () => {\n      cancelAnimationFrame(raf);\n      ro?.disconnect();\n    };\n  }, [enabled, update, floatingRef]);\n\n  \/\/ Park off-screen when closed so the next open re-measures from scratch.\n  useEffect(() => {\n    if (!enabled) {\n      setPosition((prev) =>\n        prev.x === -9999 ? prev : { x: -9999, y: -9999, placement },\n      );\n    }\n  }, [enabled, placement]);\n\n  \/\/ Reposition on scroll\/resize while open.\n  useEffect(() => {\n    if (!enabled) return;\n\n    window.addEventListener(\"scroll\", update, true);\n    window.addEventListener(\"resize\", update);\n\n    return () => {\n      window.removeEventListener(\"scroll\", update, true);\n      window.removeEventListener(\"resize\", update);\n    };\n  }, [update, enabled]);\n\n  return position;\n}\n","type":"registry:lib","target":"components\/fancy\/lib\/hooks\/use-floating-position.ts"},{"path":"components\/fancy\/lib\/hooks\/use-outside-click.ts","content":"import { useEffect, type RefObject } from \"react\";\n\nexport function useOutsideClick(\n  ref: RefObject<HTMLElement | null>,\n  handler: (event: MouseEvent | TouchEvent) => void,\n  enabled: boolean = true,\n  ignoreRef?: RefObject<HTMLElement | null>,\n): void {\n  useEffect(() => {\n    if (!enabled) return;\n\n    const listener = (event: MouseEvent | TouchEvent) => {\n      const el = ref.current;\n      if (!el || el.contains(event.target as Node)) return;\n      \/\/ Ignore clicks on the trigger\/anchor element \u2014 the trigger's\n      \/\/ own onClick handler manages toggling.\n      if (ignoreRef?.current?.contains(event.target as Node)) return;\n      handler(event);\n    };\n\n    document.addEventListener(\"mousedown\", listener);\n    document.addEventListener(\"touchstart\", listener);\n\n    return () => {\n      document.removeEventListener(\"mousedown\", listener);\n      document.removeEventListener(\"touchstart\", listener);\n    };\n  }, [ref, handler, enabled, ignoreRef]);\n}\n","type":"registry:lib","target":"components\/fancy\/lib\/hooks\/use-outside-click.ts"},{"path":"components\/fancy\/lib\/hooks\/use-escape-key.ts","content":"import { createContext, useContext, useEffect, useRef } from \"react\";\n\n\/**\n * Escape belongs to the innermost open layer.\n *\n * Every `useEscapeKey(handler, enabled)` call is a LAYER while `enabled`: a\n * Dropdown's menu, a Select's listbox, a Popover, a Modal, a Drawer, a Tooltip,\n * a full-screen surface. They all join one stack, one document listener reads\n * each Escape, and only one layer gets it.\n *\n * Why not one document listener per layer, as this hook used to do: every\n * open layer then received the same Escape. A Dropdown inside a Modal closed\n * the menu AND the dialog. Inside a full-screen surface it was worse: the\n * surface's own key handler runs during the event's trip through the surface,\n * before any document listener, so it could not tell that a menu wanted the\n * key. DeckEditor exited full screen with its Shape menu still open. Fixing\n * that by ordering listeners would only last until someone registers one in a\n * different place. So the decision is made from the stack instead.\n *\n * \"Innermost\" is decided in two steps:\n *\n * 1. The most recently opened layer is the candidate. Opening a menu from a\n *    dialog is the normal order of things.\n * 2. A full-screen surface knows its own descendants: layers rendered inside\n *    it register under it through `EscapeLayerContext`, and that context flows\n *    through portals. If the candidate is a surface with open layers inside,\n *    the most recent of those wins instead. Without this step, a menu opened\n *    BEFORE an agent put its surface full screen would lose the Escape to the\n *    surface.\n *\n * An Escape that something already handled (`event.defaultPrevented`, for\n * example a text field clearing itself) is left alone.\n *\/\n\ninterface LayerEntry {\n  handler: { current: () => void };\n  \/** The surface this layer was rendered inside, if any. *\/\n  parent: EscapeScope | null;\n  \/** Set when the layer IS a surface: layers inside it point here. *\/\n  scope: EscapeScope | null;\n}\n\n\/** Identity of a full-screen surface for the layers rendered inside it. *\/\nexport type EscapeScope = object;\n\nconst stack: LayerEntry[] = [];\n\n\/** The surface (if any) that layers rendered here belong to. *\/\nexport const EscapeLayerContext = createContext<EscapeScope | null>(null);\n\nfunction innermost(): LayerEntry | undefined {\n  let candidate = stack[stack.length - 1];\n  while (candidate?.scope) {\n    const scope = candidate.scope;\n    let inner: LayerEntry | undefined;\n    for (let i = stack.length - 1; i >= 0; i--) {\n      if (stack[i].parent === scope) {\n        inner = stack[i];\n        break;\n      }\n    }\n    if (!inner) break;\n    candidate = inner;\n  }\n  return candidate;\n}\n\nfunction onDocumentKeyDown(event: KeyboardEvent) {\n  if (event.key !== \"Escape\" || event.defaultPrevented) return;\n  const target = innermost();\n  if (!target) return;\n  event.preventDefault();\n  target.handler.current();\n}\n\nfunction push(entry: LayerEntry): () => void {\n  if (stack.length === 0) document.addEventListener(\"keydown\", onDocumentKeyDown);\n  stack.push(entry);\n  return () => {\n    const i = stack.indexOf(entry);\n    if (i >= 0) stack.splice(i, 1);\n    if (stack.length === 0) document.removeEventListener(\"keydown\", onDocumentKeyDown);\n  };\n}\n\n\/**\n * Register a layer that closes on Escape while `enabled`. Pass `scope` only for\n * a surface that other layers can open inside (a full-screen surface).\n *\n * Returns `isInnermost()`, for a layer that also handles Escape in its own key\n * handler and must not act when a layer above it owns the key.\n *\n * @internal Exported for full-screen surfaces in sibling packages; ordinary\n * layers call `useEscapeKey`.\n *\/\nexport function useEscapeLayer(\n  handler: () => void,\n  enabled: boolean,\n  scope: EscapeScope | null = null,\n): { isInnermost: () => boolean } {\n  const parent = useContext(EscapeLayerContext);\n  const handlerRef = useRef(handler);\n  handlerRef.current = handler;\n  const entryRef = useRef<LayerEntry | null>(null);\n\n  \/\/ Depends on `enabled` (and identity), NOT on `handler`: callers pass inline\n  \/\/ callbacks, and re-registering on every render would move a dialog above\n  \/\/ the menu it had just opened.\n  useEffect(() => {\n    if (!enabled) return;\n    const entry: LayerEntry = { handler: handlerRef, parent, scope };\n    entryRef.current = entry;\n    const pop = push(entry);\n    return () => {\n      pop();\n      if (entryRef.current === entry) entryRef.current = null;\n    };\n  }, [enabled, parent, scope]);\n\n  const isInnermostRef = useRef(() => entryRef.current !== null && innermost() === entryRef.current);\n  return { isInnermost: isInnermostRef.current };\n}\n\nexport function useEscapeKey(handler: () => void, enabled: boolean = true): void {\n  useEscapeLayer(handler, enabled);\n}\n","type":"registry:lib","target":"components\/fancy\/lib\/hooks\/use-escape-key.ts"}]}