useMergeSplit
trovecn/use-merge-splitFreeHook
Groups a checked-item list's contiguous rows into runs and reports one spring-animated background block per run, so adjacent selections read as one continuous shape that merges and splits instead of separate highlighted rows.
Install it
npx shadcn@latest add https://trovecn.dev/r/use-merge-split.jsonSource
1"use client";23import { useEffect, useMemo, useRef } from "react";45import type { ItemRect } from "@/hooks/use-proximity-hover";67export interface MergeSplitBlock {8 key: string;9 indices: number[];10 top: number;11 left: number;12 width: number;13 height: number;14 initialRect?: { top: number; left: number; width: number; height: number };15}1617export interface MergeSplitChange {18 index: number;19 rect: Rect;20 checked: boolean;21 key: string;22}2324export interface MergeSplitResult {25 blocks: MergeSplitBlock[];26 change: MergeSplitChange | null;27}2829interface Rect {30 top: number;31 left: number;32 width: number;33 height: number;34}3536interface RunRecord {37 key: string;38 indices: number[];39 rect: Rect;40}4142function computeRuns(43 checkedIndices: readonly number[],44 itemRects: readonly ItemRect[],45): RunRecord[] {46 const sorted = [...checkedIndices].toSorted((a, b) => a - b);47 const groups: number[][] = [];48 for (const idx of sorted) {49 const current = groups[groups.length - 1];50 if (current && idx === current[current.length - 1] + 1) {51 current.push(idx);52 } else {53 groups.push([idx]);54 }55 }5657 const runs: RunRecord[] = [];58 for (const indices of groups) {59 const first = itemRects[indices[0]];60 const last = itemRects[indices[indices.length - 1]];61 // Not measured yet (item just registered, layout pending) — skip this62 // run for now rather than publish a zeroed rect; it appears once63 // useProximityHover's own measurement pass settles.64 if (!first || !last) continue;65 runs.push({66 key: `run:${indices[0]}-${indices[indices.length - 1]}`,67 indices,68 rect: {69 top: first.top,70 left: first.left,71 width: first.width,72 height: last.top + last.height - first.top,73 },74 });75 }76 return runs;77}7879function claimUniqueKey(preferredKey: string, claimedKeys: Set<string>) {80 if (!claimedKeys.has(preferredKey)) {81 claimedKeys.add(preferredKey);82 return preferredKey;83 }8485 let suffix = 2;86 while (claimedKeys.has(`${preferredKey}:${suffix}`)) suffix += 1;87 const key = `${preferredKey}:${suffix}`;88 claimedKeys.add(key);89 return key;90}9192function reconcile(93 newRuns: RunRecord[],94 prevRuns: RunRecord[],95 originIndex: number | null,96 itemRects: readonly ItemRect[],97): MergeSplitBlock[] {98 const claimedPrimaryKeys = new Set<string>();99 const outputKeys = new Set<string>();100101 return newRuns.map((run) => {102 const runIndexSet = new Set(run.indices);103// … truncated
Files it writes
More from trovecn
All 26 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| useProximityHoveruse-proximity-hover | trovecn | Hooks | Free | no deps |
