{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-list",
  "title": "useList",
  "description": "Array state with every mutation you keep hand-writing — push, insertAt, move, swap and keyed upsert, all batch-safe, out-of-range-safe, and reference-stable.",
  "files": [
    {
      "path": "src/registry/hooks/use-list.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\n/**\n * Every empty list shares one reference: repeated `clear()` — or a filter that\n * removes everything — leaves `items` with the same identity, so a consumer with\n * `items` in a dependency array is never woken by a meaningless change.\n */\nconst EMPTY: readonly never[] = []\n\n/** The identity `getKey` returns. It is compared with `===`, so strings and numbers only. */\nexport type ListKey = string | number\n\n/** The initial list. A function is **lazy initialisation**, called once at mount, as in `useState`. */\nexport type ListInitializer<T> = readonly T[] | (() => readonly T[])\n\n/** A value or an updater for the whole list, with the semantics of a `useState` setter. */\nexport type ListSetter<T> = readonly T[] | ((prev: readonly T[]) => readonly T[])\n\n/** A value or an updater for a single item. */\nexport type ItemSetter<T> = T | ((prev: T) => T)\n\nexport interface UseListOptions<T> {\n  /**\n   * Stable identity for an item. **Must be pure** — it runs inside a state\n   * updater, so StrictMode calls it twice — and must return the same value for\n   * the same item forever.\n   *\n   * It is what unlocks `updateByKey` / `removeByKey` / `upsert`, which the types\n   * only expose once it is passed; without it the three exist but are no-ops.\n   */\n  getKey?: (item: T) => ListKey\n}\n\nexport interface UseListResult<T> {\n  /**\n   * The current list. **Read-only** — doing the immutable updates for you is the\n   * whole point of this hook, so never mutate it; copy with `[...items]` for an\n   * API that wants a mutable array. The reference is stable while the contents are.\n   */\n  items: readonly T[]\n  /** Shorthand for `items.length`. */\n  length: number\n  /** `items.length === 0`. */\n  isEmpty: boolean\n\n  /** Replace the whole list with a value or `(prev) => next`. Stable identity. */\n  set: (next: ListSetter<T>) => void\n  /**\n   * Swap in a whole new list **and make it the new baseline for `reset()`** —\n   * this is for server data landing or another document being opened, after\n   * which `reset()` returns to that list rather than the `initial` from mount.\n   * Use `set` to change the contents without touching the baseline. Stable identity.\n   */\n  replace: (next: ListSetter<T>) => void\n  /** Back to the baseline: the `initial` from mount, or the last `replace` value. Stable identity. */\n  reset: () => void\n\n  /** Append to the end. Takes several items at once; passing none is a no-op. Stable identity. */\n  push: (...items: T[]) => void\n  /** Insert at the front, keeping the order of the items among themselves. Stable identity. */\n  unshift: (...items: T[]) => void\n  /** Drop the last item; a no-op on an empty list. **Does not return it** (see \"why pop returns nothing\" below). Stable identity. */\n  pop: () => void\n  /** Drop the first item; a no-op on an empty list. It does not return it either. Stable identity. */\n  shift: () => void\n\n  /**\n   * Insert at `index`. The index is **clamped** into `[0, length]`:\n   * `insertAt(999, x)` appends, `insertAt(-4, x)` prepends, and `NaN` /\n   * `Infinity` count as an append. Never throws. Stable identity.\n   */\n  insertAt: (index: number, ...items: T[]) => void\n  /**\n   * Rewrite item `index` with a value or `(prev) => next`. An out-of-range index\n   * is **a no-op**, not clamped — writing the wrong row is far more dangerous\n   * than doing nothing. No re-render when the new value is `Object.is` the old\n   * one. Stable identity.\n   */\n  updateAt: (index: number, next: ItemSetter<T>) => void\n  /** Drop item `index`. Out of range (including `removeAt(-1)`) is a no-op; negative indices do not count from the end. Stable identity. */\n  removeAt: (index: number) => void\n\n  /** Drop **every** item matching `predicate`; no re-render when nothing matches. Stable identity. */\n  remove: (predicate: (item: T, index: number) => boolean) => void\n  /**\n   * Filter **in place**: keep the items matching `predicate` and write them back\n   * to state — it returns nothing. For a derived view just write\n   * `items.filter(...)`. Stable identity.\n   */\n  filter: (predicate: (item: T, index: number) => boolean) => void\n  /**\n   * Sort **in place** and write back to state. Without a comparator this is\n   * `Array.prototype.sort`'s default (string comparison, so `[10, 9]` stays\n   * `[10, 9]`) — always pass one for numbers. No re-render when the order comes\n   * out unchanged. Stable identity.\n   */\n  sort: (compare?: (a: T, b: T) => number) => void\n\n  /**\n   * Move the item at `from` to `to` — drag-to-reorder semantics: lift it out,\n   * then insert. Both indices are **clamped** into `[0, length - 1]`, so a drag\n   * past either end lands on that end, which is exactly what a drag wants.\n   * A no-op when the clamped indices match, the list is empty, or an index is\n   * not finite. Stable identity.\n   */\n  move: (from: number, to: number) => void\n  /**\n   * Swap two positions. Unlike `move`, this **does not clamp**: either index out\n   * of range makes the whole call a no-op — a swap is a symmetric operation on\n   * two named slots, and quietly sliding the out-of-range end onto the edge\n   * would swap the wrong row. Identical indices are a no-op too. Stable identity.\n   */\n  swap: (a: number, b: number) => void\n\n  /** Clear. No re-render when it is already empty. Stable identity. */\n  clear: () => void\n\n  /**\n   * Index of an item, `-1` when it is not there. With `getKey` the comparison is\n   * by key, otherwise by `Object.is` — so an object has to be the same reference.\n   *\n   * This is a **read** method: its identity changes whenever `items` does, unlike\n   * the write methods.\n   */\n  indexOf: (item: T) => number\n  /** `indexOf(item) !== -1`. A read method too, so its identity changes with `items`. */\n  has: (item: T) => boolean\n}\n\nexport interface UseKeyedListResult<T> {\n  /** Rewrite one item by key with a value or `(prev) => next`; an unknown key is a no-op. Stable identity. */\n  updateByKey: (key: ListKey, next: ItemSetter<T>) => void\n  /** Drop one item by key; an unknown key is a no-op. Stable identity. */\n  removeByKey: (key: ListKey) => void\n  /** **Replaces the whole item** when the key exists, appends to the end when it does not. Stable identity. */\n  upsert: (item: T) => void\n}\n\ninterface ListState<T> {\n  items: readonly T[]\n  /** Where `reset()` lands: the `initial` from mount, or the last `replace` value. */\n  baseline: readonly T[]\n}\n\nfunction resolveInitial<T>(initial: ListInitializer<T>): readonly T[] {\n  const value = typeof initial === \"function\" ? initial() : initial\n  // Copy it: a consumer mutating their own array afterwards cannot reach state\n  // or the reset baseline.\n  return value.length === 0 ? EMPTY : Array.from(value)\n}\n\nfunction resolveList<T>(next: ListSetter<T>, prev: readonly T[]): readonly T[] {\n  return typeof next === \"function\" ? next(prev) : next\n}\n\nfunction resolveItem<T>(next: ItemSetter<T>, prev: T): T {\n  return typeof next === \"function\" ? (next as (prev: T) => T)(prev) : next\n}\n\n/** Normalise to an integer; a non-finite value (`NaN` / `±Infinity`) returns `null` and the caller picks between clamping and a no-op. */\nfunction toInt(value: number): number | null {\n  return Number.isFinite(value) ? Math.trunc(value) : null\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n  return value < min ? min : value > max ? max : value\n}\n\n/** Item-by-item reference compare, so a sort that changed nothing can hand back the original array and skip a pointless re-render. */\nfunction sameOrder<T>(a: readonly T[], b: readonly T[]): boolean {\n  if (a.length !== b.length) return false\n  for (let i = 0; i < a.length; i += 1) {\n    if (!Object.is(a[i], b[i])) return false\n  }\n  return true\n}\n\n/**\n * The operations an array in state actually needs — so nobody has to hand-write\n * `setItems(prev => [...prev.slice(0, i), next, ...prev.slice(i + 1)])` all over\n * the place, which is both verbose and easy to get wrong.\n *\n * ```tsx\n * const todos = useList<Todo>([], { getKey: t => t.id })\n * todos.push({ id: crypto.randomUUID(), text, done: false })\n * todos.updateByKey(id, prev => ({ ...prev, done: !prev.done }))\n * todos.move(from, to)\n * ```\n *\n * - **Consecutive writes in one event do not overwrite each other.** Every write\n *   method goes through the same `setItems(prev => …)` functional update, so\n *   `push(a); push(b); push(c)` all land. The classic hand-written bug is exactly\n *   `setItems([...items, x])` — `items` is this frame's closure snapshot, so\n *   three calls in one event leave only the last.\n * - **Out-of-range and invalid arguments are always safe, and never throw.** Two\n *   strategies, picked by what the operation means: insert / move **clamp**\n *   (`insertAt(999)` appends, a `move` dragged past the end lands on that end);\n *   targeted read/write and swap are **a no-op out of range** (`removeAt(-1)`,\n *   `updateAt(99)` and `swap(1, 99)` all do nothing) — writing the wrong row is\n *   more dangerous than doing nothing. `NaN` / `Infinity` are refused as well.\n * - **No change, no re-render.** Whenever the result is equivalent to the current\n *   list the operation hands back the previous array reference and React bails\n *   out (`clear()` on an empty list, `swap(2, 2)`, a sort that changed no order,\n *   an `updateAt` writing an `Object.is` equal value).\n * - **Write methods have stable identities.** All `useCallback([])`, reading state\n *   only from the updater's `prev`, so they never jitter in a dependency array or\n *   in `React.memo` props, and never cause `Maximum update depth exceeded`. The\n *   **read** methods (`indexOf` / `has`) are the exception: they have to see the\n *   current `items`, so their identity changes along with it.\n * - **StrictMode safe.** Every updater is pure: ids, timestamps and keys are\n *   computed **outside** the updater (in the event handler) and passed in, never\n *   generated inside it — StrictMode's double invoke would otherwise produce two\n *   different values. Updaters you pass in have to keep the same rule.\n * - **`initial` is read once, at mount** (lazy initialisation); a new literal\n *   later does not overwrite the current list, and `reset()` returns to that\n *   snapshot. When the data comes from the server use `replace(next)`, which\n *   swaps the baseline too, so `reset()` returns to the server's copy rather than\n *   to an empty list.\n * - **Why don't `pop` / `shift` return the item they dropped?** Writes are queued:\n *   call `pop()` twice in one event and state has not moved by the second call, so\n *   any return value could only lie. Read the item off `items` before the call\n *   (`items[items.length - 1]`).\n * - **When `T` is a function type** avoid the updater form (the same old `useState`\n *   problem): `updateAt(i, fn)` treats `fn` as the updater, so write\n *   `updateAt(i, () => fn)`.\n */\nexport function useList<T>(\n  initial: ListInitializer<T>,\n  options: UseListOptions<T> & { getKey: (item: T) => ListKey },\n): UseListResult<T> & UseKeyedListResult<T>\nexport function useList<T>(\n  initial: ListInitializer<T>,\n  options?: UseListOptions<T>,\n): UseListResult<T>\nexport function useList<T>(\n  initial: ListInitializer<T>,\n  options: UseListOptions<T> = {},\n): UseListResult<T> & UseKeyedListResult<T> {\n  const { getKey } = options\n\n  // The baseline lives in state next to items, not in a ref: lazy initialisation\n  // runs once, render neither reads nor writes a ref, and reset needs no\n  // render-phase side effect.\n  const [state, setState] = React.useState<ListState<T>>(() => {\n    const first = resolveInitial(initial)\n    return { items: first, baseline: first }\n  })\n\n  // getKey rides a latest-ref: consumers will almost always pass an inline arrow,\n  // and putting that in a dependency array would swap the identity of\n  // updateByKey / removeByKey / upsert every frame. It is read in the event\n  // handler only — read first, then pass into the updater, never read in there.\n  const getKeyRef = React.useRef(getKey)\n  React.useEffect(() => {\n    getKeyRef.current = getKey\n  })\n\n  /** The one write path. `transform` must be pure — StrictMode double-invokes it. */\n  const apply = React.useCallback((transform: (prev: readonly T[]) => readonly T[]) => {\n    setState(prev => {\n      const next = transform(prev.items)\n      const items = next.length === 0 ? EMPTY : next\n      // Compare after normalising to EMPTY: `set(() => [])` hands in a brand-new\n      // empty array that is !== prev.items until then, and skipping this compare\n      // would cost a wasted render.\n      if (items === prev.items) return prev\n      return { items, baseline: prev.baseline }\n    })\n  }, [])\n\n  const set = React.useCallback(\n    (next: ListSetter<T>) => {\n      apply(prev => resolveList(next, prev))\n    },\n    [apply],\n  )\n\n  const replace = React.useCallback((next: ListSetter<T>) => {\n    setState(prev => {\n      const resolved = resolveList(next, prev.items)\n      const items = resolved.length === 0 ? EMPTY : resolved\n      if (items === prev.items && items === prev.baseline) return prev\n      return { items, baseline: items }\n    })\n  }, [])\n\n  const reset = React.useCallback(() => {\n    setState(prev => (prev.items === prev.baseline ? prev : { ...prev, items: prev.baseline }))\n  }, [])\n\n  const push = React.useCallback(\n    (...items: T[]) => {\n      if (items.length === 0) return\n      apply(prev => [...prev, ...items])\n    },\n    [apply],\n  )\n\n  const unshift = React.useCallback(\n    (...items: T[]) => {\n      if (items.length === 0) return\n      apply(prev => [...items, ...prev])\n    },\n    [apply],\n  )\n\n  const pop = React.useCallback(() => {\n    apply(prev => (prev.length === 0 ? prev : prev.slice(0, -1)))\n  }, [apply])\n\n  const shift = React.useCallback(() => {\n    apply(prev => (prev.length === 0 ? prev : prev.slice(1)))\n  }, [apply])\n\n  const insertAt = React.useCallback(\n    (index: number, ...items: T[]) => {\n      if (items.length === 0) return\n      apply(prev => {\n        // Clamp instead of refusing: an insert means \"put it around here\", so a drag\n        // past the list should land on an end.\n        const raw = toInt(index)\n        const at = raw === null ? prev.length : clamp(raw, 0, prev.length)\n        return [...prev.slice(0, at), ...items, ...prev.slice(at)]\n      })\n    },\n    [apply],\n  )\n\n  const updateAt = React.useCallback(\n    (index: number, next: ItemSetter<T>) => {\n      apply(prev => {\n        const at = toInt(index)\n        if (at === null || at < 0 || at >= prev.length) return prev\n        const current = prev[at]\n        const value = resolveItem(next, current)\n        if (Object.is(value, current)) return prev\n        const copy = prev.slice()\n        copy[at] = value\n        return copy\n      })\n    },\n    [apply],\n  )\n\n  const removeAt = React.useCallback(\n    (index: number) => {\n      apply(prev => {\n        const at = toInt(index)\n        if (at === null || at < 0 || at >= prev.length) return prev\n        return [...prev.slice(0, at), ...prev.slice(at + 1)]\n      })\n    },\n    [apply],\n  )\n\n  const remove = React.useCallback(\n    (predicate: (item: T, index: number) => boolean) => {\n      apply(prev => {\n        const next = prev.filter((item, index) => !predicate(item, index))\n        return next.length === prev.length ? prev : next\n      })\n    },\n    [apply],\n  )\n\n  const filter = React.useCallback(\n    (predicate: (item: T, index: number) => boolean) => {\n      apply(prev => {\n        const next = prev.filter(predicate)\n        return next.length === prev.length ? prev : next\n      })\n    },\n    [apply],\n  )\n\n  const sort = React.useCallback(\n    (compare?: (a: T, b: T) => number) => {\n      apply(prev => {\n        const next = prev.slice().sort(compare)\n        return sameOrder(prev, next) ? prev : next\n      })\n    },\n    [apply],\n  )\n\n  const move = React.useCallback(\n    (from: number, to: number) => {\n      apply(prev => {\n        if (prev.length === 0) return prev\n        const rawFrom = toInt(from)\n        const rawTo = toInt(to)\n        if (rawFrom === null || rawTo === null) return prev\n        const start = clamp(rawFrom, 0, prev.length - 1)\n        const end = clamp(rawTo, 0, prev.length - 1)\n        if (start === end) return prev\n        const copy = prev.slice()\n        const [moved] = copy.splice(start, 1)\n        copy.splice(end, 0, moved)\n        return copy\n      })\n    },\n    [apply],\n  )\n\n  const swap = React.useCallback(\n    (a: number, b: number) => {\n      apply(prev => {\n        const first = toInt(a)\n        const second = toInt(b)\n        if (first === null || second === null) return prev\n        if (first < 0 || first >= prev.length || second < 0 || second >= prev.length) return prev\n        if (first === second) return prev\n        const copy = prev.slice()\n        copy[first] = prev[second]\n        copy[second] = prev[first]\n        return copy\n      })\n    },\n    [apply],\n  )\n\n  const clear = React.useCallback(() => {\n    apply(prev => (prev.length === 0 ? prev : EMPTY))\n  }, [apply])\n\n  const updateByKey = React.useCallback(\n    (key: ListKey, next: ItemSetter<T>) => {\n      const keyOf = getKeyRef.current\n      if (!keyOf) return\n      apply(prev => {\n        const at = prev.findIndex(item => keyOf(item) === key)\n        if (at === -1) return prev\n        const current = prev[at]\n        const value = resolveItem(next, current)\n        if (Object.is(value, current)) return prev\n        const copy = prev.slice()\n        copy[at] = value\n        return copy\n      })\n    },\n    [apply],\n  )\n\n  const removeByKey = React.useCallback(\n    (key: ListKey) => {\n      const keyOf = getKeyRef.current\n      if (!keyOf) return\n      apply(prev => {\n        const at = prev.findIndex(item => keyOf(item) === key)\n        if (at === -1) return prev\n        return [...prev.slice(0, at), ...prev.slice(at + 1)]\n      })\n    },\n    [apply],\n  )\n\n  const upsert = React.useCallback(\n    (item: T) => {\n      const keyOf = getKeyRef.current\n      if (!keyOf) return\n      // The key is computed outside the updater, which StrictMode double-invokes;\n      // inside it only a pure lookup happens.\n      const key = keyOf(item)\n      apply(prev => {\n        const at = prev.findIndex(existing => keyOf(existing) === key)\n        if (at === -1) return [...prev, item]\n        if (Object.is(prev[at], item)) return prev\n        const copy = prev.slice()\n        copy[at] = item\n        return copy\n      })\n    },\n    [apply],\n  )\n\n  const items = state.items\n\n  // Read methods have to see this frame's items, so they cannot ride a latest-ref:\n  // a ref read during render returns the previous frame, and a consumer calling\n  // has() in JSX would get a stale answer. Their identity changes with items.\n  const indexOf = React.useCallback(\n    (item: T) => {\n      if (getKey) {\n        const key = getKey(item)\n        return items.findIndex(existing => getKey(existing) === key)\n      }\n      return items.findIndex(existing => Object.is(existing, item))\n    },\n    [items, getKey],\n  )\n\n  const has = React.useCallback((item: T) => indexOf(item) !== -1, [indexOf])\n\n  return React.useMemo(\n    () => ({\n      items,\n      length: items.length,\n      isEmpty: items.length === 0,\n      set,\n      replace,\n      reset,\n      push,\n      unshift,\n      pop,\n      shift,\n      insertAt,\n      updateAt,\n      removeAt,\n      remove,\n      filter,\n      sort,\n      move,\n      swap,\n      clear,\n      indexOf,\n      has,\n      updateByKey,\n      removeByKey,\n      upsert,\n    }),\n    [\n      items,\n      set,\n      replace,\n      reset,\n      push,\n      unshift,\n      pop,\n      shift,\n      insertAt,\n      updateAt,\n      removeAt,\n      remove,\n      filter,\n      sort,\n      move,\n      swap,\n      clear,\n      indexOf,\n      has,\n      updateByKey,\n      removeByKey,\n      upsert,\n    ],\n  )\n}\n\nexport default useList\n",
      "type": "registry:hook"
    }
  ],
  "type": "registry:hook"
}