# Utils

> Generated reference (regen-owned; see [index.md](index.md)). Binds only under `primitives: nurix`.

Non-visual helpers, each published as its own subpath under `@nurix/components/lib/<name>`. Class merging on an ordinary element is the standalone `cn` package (`import { cn } from "cn"`, installed with the library), not a subpath here.

## mergeBaseClassName

`mergeBaseClassName(ours, theirs)` (from `lib/utils`) — merges a wrapper's own classes with a caller's `className` on a Base UI part, which types `className` as `string | ((state) => string | undefined)`. The result is always the callback form, so a caller styling off part state keeps working; bare `cn` accepts the callback, then silently drops it. The result is a function even for two strings, so never pass it where a `string` is expected. The same subpath exports `isMobileDevice()`, a userAgent regex with no reactivity; branch on `useMediaQuery` (`hooks/use-media-query`) instead.

```tsx
<BasePopover.Popup className={mergeBaseClassName("rounded-md p-3", className)} />
```

## formatDate

`formatDate(date, opts?, locale?)` (from `lib/format`) — an absolute stamp through `Intl.DateTimeFormat`. `opts` is a patch against a seeded `{ month: "long", day: "numeric", year: "numeric" }`, not a verbatim `Intl` bag: remove a field by naming it `undefined`, because omitting it leaves the seed in place. A `dateStyle`/`timeStyle` bag passes unseeded. Omitting `locale` resolves it from the runtime. A missing or unformattable value returns `""` rather than throwing.

```tsx
formatDate(value, { month: "short", day: "numeric", year: undefined }); // "Aug 15"
```

## formatRelativeTime

`formatRelativeTime(ts, now?, locale?)` and `useNow(tickMs = DEFAULT_TICK_MS, active = true)` (from `lib/time`) — a relative label ("5 minutes ago", "in 2 hours") on `Intl.RelativeTimeFormat`, plus the clock that re-renders it. `DEFAULT_TICK_MS` is `30_000`. Every `useNow` subscriber at the same `tickMs` shares one interval, so call it per row; `active: false` subscribes to nothing and freezes the clock at its last tick. A bare `Date.now()` in place of `useNow()` never updates. `element/timestamp` already wires both.

```tsx
<span>{formatRelativeTime(updatedAt, useNow())}</span>
```

## generateId

`generateId({ length = 12 }?)` (from `lib/id`) — a random 62-alphanumeric id for handles that never leave the browser: a React key for a staged file, a client row id minted before the server answers. `length` is the only option; there is no prefix or separator. Ids are neither sortable nor UUIDs, so a persisted row takes a UUID. An id referenced by `aria-*` or `htmlFor` is React's `useId`, since a call-time id breaks hydration. About one id in six starts with a digit, so `querySelector("#" + id)` needs `CSS.escape`.

```tsx
const stagedFile = { key: generateId(), file };
```

## getStrictContext

`getStrictContext<T>(name?)` (from `lib/get-strict-context`) — returns a `[Provider, useSafeContext]` pair whose hook throws `useContext must be used within <name>` outside its `Provider`, so a namespaced family's misplaced part fails loudly instead of rendering empty. Call it once at module scope, since each call mints a fresh Provider identity, and pass a memoised `value`. A legitimately optional value wants a plain `React.createContext` with a default.

```tsx
const [TabsProvider, useTabs] = getStrictContext<TabsState>("Tabs");
```

## composeRefs

`composeRefs(...refs)` / `useComposedRefs(...refs)` (from `lib/compose-refs`) — merges callback refs and `RefObject`s into one callback ref, honouring React 19 ref cleanups. Use the hook when a component keeps its own ref and accepts the caller's; the measuring hooks' callback refs compose here. `composeRefs` is the non-hook form for event handlers and imperative helpers. The refs are the memo's dependency list: hoist inline callback refs, and keep the arity fixed by passing `undefined` for an absent ref.

```tsx
<div ref={useComposedRefs(forwardedRef, localRef)} />
```

The motion-aware `asChild` slot, `Slot` (`lib/slot`), is a motion primitive — [motion.md](motion.md).

## readableOn

`readableOn(color)` (from `lib/color`) — white or near-black ink (`"#ffffff"` / `"#1a1a1a"`) for a fill by WCAG relative luminance, split at 0.55. Reach for it only when the fill comes from outside the token layer, such as an initials avatar on a brand seed; a themed surface takes `text-foreground`. Input must be a 6-digit hex with or without `#`; a CSS var, an `oklch()` or a 3-digit hex silently answers `"#ffffff"`. Set fill and ink on the same element so they never drift.

```tsx
<span style={{ backgroundColor: seed, color: readableOn(seed) }}>{initials}</span>
```

## upgradeConfig

`upgradeConfig(input, currentVersion, migrations?)` (from `lib/config-version`) — runs once wherever a stored dashboard or chart config is read back. Versionless input reads as version 1, and `migrations[n]` lifts a config from `n` to `n + 1`. A version with no registered migration passes through unchanged, and unknown fields survive only when each migration spreads its input. The result is always stamped `version: currentVersion`, so a missing migration is invisible afterwards; `undefined` input yields `{ version: currentVersion }`. A config that never leaves memory owes no version.

```tsx
const config = upgradeConfig(stored, 2, { 1: (c) => ({ ...c, density: "comfortable" }) });
```
