# Sift

> Structured filter bar for any list or table: a + Filter popover that slides property → operator → value (type-ahead fields, per-type operators, checkbox listbox, number range, and a range-selecting month calendar with presets), spring-in chips you edit in place part by part, an and/or toggle, saved views that crossfade chip sets with inline Save view, Clear all, and a result count rolling on Tally drums. Controlled or uncontrolled, keyboard-first (F, arrows, Backspace), with a pure applyFilters() helper to drive your rows.

A shadcn/ui block from the [@crucible](https://crucibleui.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/crucible/sift).

- Page, with a live preview: https://shadcn.exchange/r/crucible/sift
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/crucible/sift-light-e251de7c077ca297.png
- Registry item: https://crucibleui.com/r/sift.json
- Listed: 2026-10-20, source last changed 2026-10-20

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/crucible/sift.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `motion`
- Registry items: `https://crucibleui.com/r/tally.json`, `https://crucibleui.com/r/use-reduced-motion.json`

## Files

### `registry/default/app/sift/sift.tsx`

```tsx
"use client";

import { AnimatePresence, LayoutGroup, motion, useInView, type Transition } from "motion/react";
import * as React from "react";

import { cn } from "@/lib/utils";
import { useReducedMotion } from "@/hooks/use-reduced-motion";
import { Tally } from "@/components/crucible/tally";

/* ================================================================ types */

/** How a field's values are compared and edited. */
export type SiftFieldType = "text" | "select" | "number" | "date" | "boolean";

/** One choice of a `select` field. */
export interface SiftOption {
  /** Stored / compared value. */
  value: string;
  /** Visible label. @default value */
  label?: string;
  /** Optional status dot color (any CSS color). */
  color?: string;
}

/** A filterable property of your rows. */
export interface SiftField {
  /** Row key read by `applyFilters` (`row[key]`). */
  key: string;
  /** Visible name in the property list and on chips. */
  label: string;
  /** Decides the operators and the value editor. */
  type: SiftFieldType;
  /** Choices for `select` fields (strings or `{ value, label, color }`). */
  options?: (string | SiftOption)[];
  /** 14px glyph (uses `currentColor`). A type glyph is used when omitted. */
  icon?: React.ReactNode;
}

/**
 * Comparison operator. Per field type:
 * text `contains | is | is-not | starts-with`; select `any-of | none-of`;
 * number `eq | neq | gt | lt | between`; date `is | before | after | between`;
 * boolean `is-true | is-false`.
 */
export type SiftOperator =
  | "contains"
  | "is"
  | "is-not"
  | "starts-with"
  | "any-of"
  | "none-of"
  | "eq"
  | "neq"
  | "gt"
  | "lt"
  | "between"
  | "before"
  | "after"
  | "is-true"
  | "is-false";

/** Relative date ranges, resolved against "today" when filtering. */
export type SiftDatePreset = "today" | "last-7" | "last-30" | "this-month";

/**
 * A filter's operand: `string` (text, or a `YYYY-MM-DD` date), `string[]`
 * (select), `number`, `[min, max]` (number between), `[from, to]` date keys
 * (date between) or `null` (boolean / preset).
 */
export type SiftFilterValue = string | number | boolean | string[] | [number, number] | [string, string] | null;

/** One applied filter (one chip). */
export interface SiftFilter {
  /** Stable id (chip key). */
  id: string;
  /** `SiftField.key` this filter targets. */
  field: string;
  operator: SiftOperator;
  value: SiftFilterValue;
  /** Date fields only: a relative range that overrides `value`. */
  preset?: SiftDatePreset;
}

/** How chips combine: every filter must match (`and`) or any (`or`). */
export type SiftConjunction = "and" | "or";

/** A named, saved chip set. */
export interface SiftView {
  id: string;
  label: string;
  filters: SiftFilter[];
  /** @default "and" */
  conjunction?: SiftConjunction;
}

/** Imperative verbs exposed through the `ref` prop. */
export interface SiftHandle {
  /** Open the filter popover — at the operator step when `fieldKey` is given. */
  open: (fieldKey?: string) => void;
  /** Close the popover (and views menu) if open. */
  close: () => void;
  /** Remove every filter. */
  clear: () => void;
  /** Replay the intro. */
  replay: () => void;
}

export interface SiftProps
  extends Omit<React.ComponentPropsWithoutRef<"div">, "onChange" | "defaultValue" | "children"> {
  /**
   * Filterable properties.
   * @default issue-tracker sample fields (Status, Priority, Assignee, Title, Estimate, Updated, Failing checks)
   */
  fields?: SiftField[];
  /** Controlled filters. Omit for uncontrolled use. */
  value?: SiftFilter[];
  /**
   * Initial filters (uncontrolled).
   * @default the "Active work" sample view when `fields` is omitted, otherwise none
   */
  defaultValue?: SiftFilter[];
  /** Fires with the full next filter list on every add, edit, removal, clear or view switch. */
  onChange?: (filters: SiftFilter[]) => void;
  /** Controlled conjunction. */
  conjunction?: SiftConjunction;
  /** Initial conjunction (uncontrolled). @default "and" */
  defaultConjunction?: SiftConjunction;
  /** Fires when the and / or toggle between chips flips. */
  onConjunctionChange?: (conjunction: SiftConjunction) => void;
  /**
   * Saved views in the views menu. Pass `[]` to hide the menu.
   * @default sample views (All issues, Active work, My open, Failing this week, Urgent & high) when `fields` is omitted
   */
  views?: SiftView[];
  /** Id of the view selected initially. @default "active" for the sample views, else the first view */
  defaultView?: string;
  /** Fires when a view is picked from the menu. */
  onViewChange?: (view: SiftView) => void;
  /**
   * Called when a view is saved from "Save view". Return a Promise to show a
   * pending state; rejecting keeps the name field open with the error. The
   * view is added to the menu on success. Omit to hide "Save view".
   */
  onSaveView?: (view: SiftView) => void | Promise<unknown>;
  /** Result count shown at the right; rolls on Tally drums when it changes. */
  count?: number;
  /** Optional total, shown as "24 of 40". */
  total?: number;
  /** Result noun. @default { one: "result", other: "results" } */
  noun?: { one: string; other: string };
  /** "Today" for date presets and the calendar (YYYY-MM-DD or Date). @default the viewer's local today */
  today?: string | Date;
  /** Accent for focus rings, the active chip, values and the Apply button. @default "#e4e4e7" */
  accentColor?: string;
  /** Imperative handle (`open(fieldKey?)`, `close()`, `clear()`, `replay()`). React 19 ref-as-prop. */
  ref?: React.Ref<SiftHandle>;
  /** Extra classes for the root. */
  className?: string;
}

/* ================================================================ constants */

const DEFAULT_ACCENT = "#e4e4e7";
const EASE: [number, number, number, number] = [0.22, 1, 0.36, 1];
const SPRING: Transition = { type: "spring", stiffness: 520, damping: 34, mass: 0.7 };
const SLIDE: Transition = { type: "spring", stiffness: 480, damping: 40 };
const FADE: Transition = { duration: 0.18, ease: "easeOut" };
/** Reduced motion: geometry snaps, only opacity fades. */
const REDUCED_T: Transition = { duration: 0, opacity: { duration: 0.18, ease: "easeOut" } };
const POP_W = 304;

const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
const MONTHS_LONG = [
  "January",
  "February",
  "March",
  "April",
  "May",
  "June",
  "July",
  "August",
  "September",
  "October",
  "November",
  "December",
];
const WEEKDAYS = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];

const PRESETS: { id: SiftDatePreset; label: string }[] = [
  { id: "today", label: "Today" },
  { id: "last-7", label: "Last 7 days" },
  { id: "last-30", label: "Last 30 days" },
  { id: "this-month", label: "This month" },
];
const PRESET_LABEL: Record<SiftDatePreset, string> = {
  today: "Today",
  "last-7": "Last 7 days",
  "last-30": "Last 30 days",
  "this-month": "This month",
};

interface OpDef {
  id: SiftOperator;
  label: string;
  hint?: string;
}

const OPS: Record<SiftFieldType, OpDef[]> = {
  text: [
    { id: "contains", label: "contains" },
    { id: "is", label: "is" },
    { id: "is-not", label: "is not" },
    { id: "starts-with", label: "starts with" },
  ],
  select: [
    { id: "any-of", label: "is any of" },
    { id: "none-of", label: "is none of" },
  ],
  number: [
    { id: "eq", label: "=", hint: "equals" },
    { id: "neq", label: "≠", hint: "not equal" },
    { id: "gt", label: ">", hint: "greater than" },
    { id: "lt", label: "<", hint: "less than" },
    { id: "between", label: "between", hint: "inclusive" },
  ],
  date: [
    { id: "is", label: "is" },
    { id: "before", label: "before" },
    { id: "after", label: "after" },
    { id: "between", label: "between" },
  ],
  boolean: [
    { id: "is-true", label: "is true" },
    { id: "is-false", label: "is false" },
  ],
};

function opLabel(type: SiftFieldType, op: SiftOperator): string {
  return OPS[type].find((o) => o.id === op)?.label ?? op;
}

/* ================================================================ icons */

const svg = (d: React.ReactNode) => (
  <svg
    viewBox="0 0 16 16"
    fill="none"
    stroke="currentColor"
    strokeWidth="1.5"
    strokeLinecap="round"
    strokeLinejoin="round"
    className="size-3.5 shrink-0"
    aria-hidden
  >
    {d}
  </svg>
);

const Icon = {
  plus: svg(<path d="M8 3.5v9M3.5 8h9" />),
  x: svg(<path d="m4.5 4.5 7 7m0-7-7 7" />),
  back: svg(<path d="M9.5 3.5 5 8l4.5 4.5" />),
  chevron: svg(<path d="m4.5 6.5 3.5 3.5 3.5-3.5" />),
  chevL: svg(<path d="M9.5 4 5.5 8l4 4" />),
  chevR: svg(<path d="m6.5 4 4 4-4 4" />),
  check: svg(<path d="m3.5 8.5 3 3 6-7" />),
  search: svg(
    <>
      <circle cx="7" cy="7" r="4.25" />
      <path d="m10.25 10.25 3 3" />
    </>
  ),
  views: svg(
    <>
      <path d="M2.5 5 8 2.5 13.5 5 8 7.5z" />
      <path d="m2.5 8 5.5 2.5L13.5 8" />
      <path d="m2.5 11 5.5 2.5 5.5-2.5" />
    </>
  ),
  funnel: svg(<path d="M2.5 3.5h11l-4.25 5v4l-2.5 1.25V8.5z" />),
  text: svg(<path d="M3 12.5 6 3.5l3 9M4 9.5h4M10.5 8.5c.4-.6 1-.9 1.75-.9 1 0 1.75.6 1.75 1.6v3.3M14 10.5c-2.2 0-3.5.4-3.5 1.2 0 .6.5 1 1.2 1 1.2 0 2.3-.8 2.3-2.2" />),
  select: svg(
    <>
      <circle cx="8" cy="8" r="5.25" />
      <circle cx="8" cy="8" r="1.75" fill="currentColor" stroke="none" />
    </>
  ),
  number: svg(<path d="M6 2.5 4.75 13.5M11.25 2.5 10 13.5M3 6h10.5M2.5 10H13" />),
  date: svg(
    <>
      <rect x="2.5" y="3.5" width="11" height="10" rx="2" />
      <path d="M2.5 7h11M5.5 2v3M10.5 2v3" />
    </>
  ),
  boolean: svg(
    <>
      <rect x="1.75" y="4.5" width="12.5" height="7" rx="3.5" />
      <circle cx="10.75" cy="8" r="1.9" fill="currentColor" stroke="none" />
    </>
  ),
  status: svg(
    <>
      <circle cx="8" cy="8" r="5.25" strokeDasharray="2.2 1.6" />
      <path d="M8 4.75a3.25 3.25 0 0 1 0 6.5z" fill="currentColor" stroke="none" />
    </>
  ),
  priority: svg(<path d="M3.5 12.5v-3M8 12.5v-6M12.5 12.5v-9" strokeWidth="1.75" />),
  person: svg(
    <>
      <circle cx="8" cy="5.75" r="2.5" />
      <path d="M3 13.5c.6-2.4 2.6-3.75 5-3.75s4.4 1.35 5 3.75" />
    </>
  ),
  estimate: svg(
    <>
      <path d="M8 2.5 13.5 5.5v5L8 13.5 2.5 10.5v-5z" />
      <path d="M2.5 5.5 8 8.5l5.5-3M8 8.5v5" />
    </>
  ),
  clock: svg(
    <>
      <circle cx="8" cy="8" r="5.5" />
      <path d="M8 5v3l2 1.5" />
    </>
  ),
  alert: svg(
    <>
      <path d="M8 2.5 14 13H2z" />
      <path d="M8 6.75v2.75M8 11.25v.01" />
    </>
  ),
};

const TYPE_ICON: Record<SiftFieldType, React.ReactNode> = {
  text: Icon.text,
  select: Icon.select,
  number: Icon.number,
  date: Icon.date,
  boolean: Icon.boolean,
};

/* ================================================================ sample data */

/** Sample issue-tracker fields used when `fields` is omitted. */
export const defaultSiftFields: SiftField[] = [
  {
    key: "status",
    label: "Status",
    type: "select",
    icon: Icon.status,
    options: [
      { value: "Backlog", color: "#71717a" },
      { value: "Todo", color: "#d4d4d8" },
      { value: "In progress", color: "#fbbf24" },
      { value: "In review", color: "#818cf8" },
      { value: "Done", color: "#4ade80" },
      { value: "Canceled", color: "#52525b" },
    ],
  },
  {
    key: "priority",
    label: "Priority",
    type: "select",
    icon: Icon.priority,
    options: [
      { value: "Urgent", color: "#f87171" },
      { value: "High", color: "#fb923c" },
      { value: "Medium", color: "#facc15" },
      { value: "Low", color: "#a1a1aa" },
      { value: "None", color: "#52525b" },
    ],
  },
  {
    key: "assignee",
    label: "Assignee",
    type: "select",
    icon: Icon.person,
    options: ["Mara Quill", "Jonah Reyes", "Ines Varga", "Kofi Mensah", "Priya Raman", "Unassigned"],
  },
  { key: "title", label: "Title", type: "text", icon: Icon.text },
  { key: "estimate", label: "Estimate", type: "number", icon: Icon.estimate },
  { key: "updated", label: "Updated", type: "date", icon: Icon.clock },
  { key: "failing", label: "Failing checks", type: "boolean", icon: Icon.alert },
];

/** Sample saved views for `defaultSiftFields`. */
export const defaultSiftViews: SiftView[] = [
  { id: "all", label: "All issues", filters: [] },
  {
    id: "active",
    label: "Active work",
    filters: [
      { id: "v-active-status", field: "status", operator: "any-of", value: ["Todo", "In progress", "In review"] },
      { id: "v-active-est", field: "estimate", operator: "gt", value: 1 },
    ],
  },
  {
    id: "mine",
    label: "My open",
    filters: [
      { id: "v-mine-who", field: "assignee", operator: "any-of", value: ["Mara Quill"] },
      { id: "v-mine-status", field: "status", operator: "none-of", value: ["Done", "Canceled"] },
    ],
  },
  {
    id: "failing",
    label: "Failing this week",
    filters: [
      { id: "v-fail-bool", field: "failing", operator: "is-true", value: null },
      { id: "v-fail-date", field: "updated", operator: "is", value: null, preset: "last-7" },
    ],
  },
  {
    id: "urgent",
    label: "Urgent & high",
    filters: [{ id: "v-urgent", field: "priority", operator: "any-of", value: ["Urgent", "High"] }],
  },
];

/* ================================================================ date helpers */

const pad = (n: number) => String(n).padStart(2, "0");
function dkey(d: Date): string {
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}
function fromKey(k: string): Date {
  const [y, m, d] = k.split("-").map(Number);
  return new Date(y, (m ?? 1) - 1, d ?? 1);
}
function addDays(k: string, n: number): string {
  const d = fromKey(k);
  d.setDate(d.getDate() + n);
  return dkey(d);
}
function addMonths(month: string, n: number): string {
  const [y, m] = month.split("-").map(Number);
  const d = new Date(y, m - 1 + n, 1);
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}`;
}
/** Coerces a row value (Date, epoch ms, ISO string) to a local `YYYY-MM-DD` key. */
function toKey(v: unknown): string | null {
  if (v instanceof Date) return Number.isNaN(v.getTime()) ? null : dkey(v);
  if (typeof v === "number") return Number.isFinite(v) ? dkey(new Date(v)) : null;
  if (typeof v === "string") {
    if (/^\d{4}-\d{2}-\d{2}$/.test(v)) return v;
    const d = new Date(v);
    return Number.isNaN(d.getTime()) ? null : dkey(d);
  }
  return null;
}
function presetRange(p: SiftDatePreset, today: string): [string, string] {
  if (p === "today") return [today, today];
  if (p === "last-7") return [addDays(today, -6), today];
  if (p === "last-30") return [addDays(today, -29), today];
  const first = `${today.slice(0, 7)}-01`;
  return [first, addDays(`${addMonths(today.slice(0, 7), 1)}-01`, -1)];
}
function fmtDay(k: string): string {
  const d = fromKey(k);
  return `${MONTHS[d.getMonth()]} ${d.getDate()}`;
}

const subscribeNoop = () => () => {};
/** Local today as `YYYY-MM-DD`; null on the server / during hydration. */
function useTodayKey(override?: string | Date): string | null {
  const live = React.useSyncExternalStore(
    subscribeNoop,
    () => dkey(new Date()),
    () => null
  );
  if (override) return toKey(override);
  return live;
}

/* ================================================================ pure filtering */

function optionsOf(field: SiftField | undefined): Required<SiftOption>[] {
  return (field?.options ?? []).map((o) =>
    typeof o === "string"
      ? { value: o, label: o, color: "" }
      : { value: o.value, label: o.label ?? o.value, color: o.color ?? "" }
  );
}

function matches(row: Record<string, unknown>, f: SiftFilter, field: SiftField, today: string): boolean {
  const raw = row[field.key];
  switch (field.type) {
    case "text": {
      const hay = String(raw ?? "").toLowerCase();
      const needle = String(f.value ?? "").toLowerCase();
      if (!needle) return true;
      if (f.operator === "is") return hay === needle;
      if (f.operator === "is-not") return hay !== needle;
      if (f.operator === "starts-with") return hay.startsWith(needle);
      return hay.includes(needle);
    }
    case "select": {
      const sel = Array.isArray(f.value) ? f.value.map(String) : f.value == null ? [] : [String(f.value)];
      if (!sel.length) return true;
      const vals = Array.isArray(raw) ? raw.map(String) : raw == null ? [] : [String(raw)];
      const hit = vals.some((v) => sel.includes(v));
      return f.operator === "none-of" ? !hit : hit;
    }
    case "number": {
      const n = typeof raw === "number" ? raw : Number(raw);
      if (raw == null || Number.isNaN(n)) return false;
      if (f.operator === "between" && Array.isArray(f.value)) {
        const [a, b] = (f.value as number[]).map(Number);
        return n >= Math.min(a, b) && n <= Math.max(a, b);
      }
      const v = Number(f.value);
      if (f.operator === "neq") return n !== v;
      if (f.operator === "gt") return n > v;
      if (f.operator === "lt") return n < v;
      return n === v;
    }
    case "date": {
      const k = toKey(raw);
      if (!k) return false;
      if (f.preset) {
        const [a, b] = presetRange(f.preset, today);
        return k >= a && k <= b;
      }
      if (f.operator === "between" && Array.isArray(f.value)) {
        const [a, b] = (f.value as string[]).map(String).sort();
        return k >= a && k <= b;
      }
      const v = String(f.value ?? "");
      if (!v) return true;
      if (f.operator === "before") return k < v;
      if (f.operator === "after") return k > v;
      return k === v;
    }
    case "boolean":
      return Boolean(raw) === (f.operator === "is-true");
  }
}

/**
 * Pure helper: returns the rows that satisfy `filters` (AND by default, or OR),
 * reading `row[field.key]`. Filters for unknown fields are ignored. Date
 * values may be `Date`s, epoch ms or ISO strings; presets resolve against
 * `options.now` (default: the current date).
 */
export function applyFilters<T extends object>(
  rows: readonly T[],
  filters: readonly SiftFilter[],
  fields: readonly SiftField[] = defaultSiftFields,
  options: { conjunction?: SiftConjunction; now?: Date | string } = {}
): T[] {
  const active = filters
    .map((f) => ({ f, field: fields.find((x) => x.key === f.field) }))
    .filter((x): x is { f: SiftFilter; field: SiftField } => !!x.field);
  if (!active.length) return rows.slice();
  const today = toKey(options.now ?? new Date()) ?? dkey(new Date());
  const any = options.conjunction === "or";
  return rows.filter((row) => {
    const r = row as Record<string, unknown>;
    return any ? active.some((x) => matches(r, x.f, x.field, today)) : active.every((x) => matches(r, x.f, x.field, today));
  });
}

/* ================================================================ misc helpers */

function inkFor(hex: string): string {
  const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());
  if (!m) return "#0a0a0a";
  const h = m[1].length === 3 ? [...m[1]].map((c) => c + c).join("") : m[1];
  const [r, g, b] = [0, 2, 4].map((i) => {
    const c = parseInt(h.slice(i, i + 2), 16) / 255;
    return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
  });
  const L = 0.2126 * r + 0.7152 * g + 0.0722 * b;
  return L > 0.36 ? "#0a0a0a" : "#ffffff";
}

function sig(filters: readonly SiftFilter[], conj: SiftConjunction): string {
  return JSON.stringify([conj, filters.map((f) => [f.field, f.operator, f.value, f.preset ?? null])]);
}

function summarize(field: SiftField, f: SiftFilter): string {
  switch (field.type) {
    case "text":
      return `“${String(f.value ?? "")}”`;
    case "select": {
      const opts = optionsOf(field);
      const labels = (Array.isArray(f.value) ? f.value : []).map(
        (v) => opts.find((o) => o.value === v)?.label ?? String(v)
      );
      return labels.length > 2 ? `${labels.slice(0, 2).join(", ")} +${labels.length - 2}` : labels.join(", ");
    }
    case "number":
      return Array.isArray(f.value) ? `${f.value[0]} – ${f.value[1]}` : String(f.value ?? "");
    case "date":
      if (f.preset) return PRESET_LABEL[f.preset];
      if (Array.isArray(f.value)) {
        const [a, b] = (f.value as string[]).map(String).sort();
        return a === b ? fmtDay(a) : `${fmtDay(a)} – ${fmtDay(b)}`;
      }
      return f.value ? fmtDay(String(f.value)) : "";
    case "boolean":
      return "";
  }
}

function describe(field: SiftField, f: SiftFilter): string {
  const v = summarize(field, f);
  return `${field.label} ${opLabel(field.type, f.operator)}${v ? ` ${v}` : ""}`;
}

/* ================================================================ draft */

interface Draft {
  editingId: string | null;
  field: string | null;
  operator: SiftOperator | null;
  text: string;
  sel: string[];
  numA: string;
  numB: string;
  dateA: string | null;
  dateB: string | null;
  preset: SiftDatePreset | null;
}

const EMPTY_DRAFT: Draft = {
  editingId: null,
  field: null,
  operator: null,
  text: "",
  sel: [],
  numA: "",
  numB: "",
  dateA: null,
  dateB: null,
  preset: null,
};

function draftFrom(f: SiftFilter): Draft {
  const d: Draft = { ...EMPTY_DRAFT, editingId: f.id, field: f.field, operator: f.operator };
  const v = f.value;
  if (typeof v === "string") {
    d.text = v;
    d.dateA = /^\d{4}-\d{2}-\d{2}$/.test(v) ? v : null;
  }
  if (Array.isArray(v)) {
    if (typeof v[0] === "number") {
      d.numA = String(v[0]);
      d.numB = String(v[1] ?? "");
    } else if (f.operator === "between" && v.length === 2 && /^\d{4}-/.test(String(v[0]))) {
      d.dateA = String(v[0]);
      d.dateB = String(v[1]);
    } else d.sel = v.map(String);
  }
  if (typeof v === "number") d.numA = String(v);
  d.preset = f.preset ?? null;
  return d;
}

const isNum = (s: string) => s.trim() !== "" && Number.isFinite(Number(s));

function buildFilter(d: Draft, field: SiftField, id: string): SiftFilter | null {
  const op = d.operator;
  if (!op) return null;
  const base = { id, field: field.key, operator: op };
  switch (field.type) {
    case "text":
      return d.text.trim() ? { ...base, value: d.text.trim() } : null;
    case "select":
      return d.sel.length ? { ...base, value: d.sel } : null;
    case "number":
      if (op === "between")
        return isNum(d.numA) && isNum(d.numB)
          ? { ...base, value: [Number(d.numA), Number(d.numB)].sort((a, b) => a - b) as [number, number] }
          : null;
      return isNum(d.numA) ? { ...base, value: Number(d.numA) } : null;
    case "date":
      if (d.preset) return { ...base, operator: "is", value: null, preset: d.preset };
      if (!d.dateA) return null;
      if (op === "between") {
        const pair = [d.dateA, d.dateB ?? d.dateA].sort() as [string, string];
        return { ...base, value: pair };
      }
      return { ...base, value: d.dateA };
    case "boolean":
      return op === "is-true" || op === "is-false" ? { ...base, value: null } : null;
  }
}

/* ================================================================ small pieces */

type Step = "field" | "operator" | "value";
const STEP_INDEX: Record<Step, number> = { field: 0, operator: 1, value: 2 };

function Dot({ color }: { color: string }) {
  if (!color) return null;
  return <span aria-hidden className="size-2 shrink-0 rounded-full" style={{ background: color }} />;
}

const FOCUS =
  "outline-none focus-visible:ring-2 focus-visible:ring-[var(--sift-accent)] focus-visible:ring-offset-1 focus-visible:ring-offset-neutral-900";

/** Keyboard model shared by the listboxes: Up/Down/Home/End move, Enter picks. */
function listKeys(
  e: React.KeyboardEvent,
  count: number,
  active: number,
  setActive: (n: number) => void,
  pick: (n: number) => void
) {
  if (!count) return;
  if (e.key === "ArrowDown") {
    e.preventDefault();
    setActive((active + 1) % count);
  } else if (e.key === "ArrowUp") {
    e.preventDefault();
    setActive((active - 1 + count) % count);
  } else if (e.key === "Home") {
    e.preventDefault();
    setActive(0);
  } else if (e.key === "End") {
    e.preventDefault();
    setActive(count - 1);
  } else if (e.key === "Enter") {
    e.preventDefault();
    pick(active);
  }
}

function useFocusOnMount<T extends HTMLElement>() {
  const ref = React.useRef<T>(null);
  React.useEffect(() => {
    ref.current?.focus({ preventScroll: true });
  }, []);
  return ref;
}

/* ---------------------------------------------------------------- field step */

function FieldStep({
  uid,
  fields,
  selected,
  onPick,
}: {
  uid: string;
  fields: SiftField[];
  selected: string | null;
  onPick: (key: string) => void;
}) {
  const [q, setQ] = React.useState("");
  const [active, setActive] = React.useState(() => Math.max(0, fields.findIndex((f) => f.key === selected)));
  const input = useFocusOnMount<HTMLInputElement>();
  const shown = React.useMemo(() => {
    const s = q.trim().toLowerCase();
    return s ? fields.filter((f) => f.label.toLowerCase().includes(s)) : fields;
  }, [fields, q]);
  const act = Math.min(active, shown.length - 1);
  const listId = `${uid}-fields`;

  return (
    <div className="p-1.5">
      <label className="relative flex items-center">
        <span className="pointer-events-none absolute left-2.5 text-white/35">{Icon.search}</span>
        <input
          ref={input}
          value={q}
          onChange={(e) => {
            setQ(e.target.value);
            setActive(0);
          }}
          onKeyDown={(e) => listKeys(e, shown.length, act, setActive, (i) => shown[i] && onPick(shown[i].key))}
          role="combobox"
          aria-expanded
          aria-controls={listId}
          aria-activedescendant={shown[act] ? `${listId}-${shown[act].key}` : undefined}
          aria-label="Filter by property"
          placeholder="Filter by…"
          className="h-9 w-full rounded-lg border border-white/[0.08] bg-white/[0.03] pl-8 pr-2.5 text-[13px] text-white placeholder:text-white/35 outline-none transition-colors focus:border-[color-mix(in_srgb,var(--sift-accent)_45%,transparent)]"
        />
      </label>
      <ul id={listId} role="listbox" aria-label="Properties" className="mt-1.5 max-h-64 overflow-y-auto">
        {shown.map((f, i) => (
          <li
            key={f.key}
            id={`${listId}-${f.key}`}
            role="option"
            aria-selected={i === act}
            onPointerMove={() => setActive(i)}
            onClick={() => onPick(f.key)}
            className={cn(
              "flex h-8 cursor-pointer items-center gap-2.5 rounded-md px-2.5 text-[13px] text-white/75",
              i === act && "bg-white/[0.06] text-white"
            )}
          >
            <span className="text-white/45">{f.icon ?? TYPE_ICON[f.type]}</span>
            <span className="flex-1 truncate">{f.label}</span>
            {f.key === selected && <span className="text-[var(--sift-accent)]">{Icon.check}</span>}
          </li>
        ))}
        {!shown.length && <li className="px-2.5 py-3 text-[12.5px] text-white/40">No matching properties</li>}
      </ul>
    </div>
  );
}

/* ---------------------------------------------------------------- operator step */

function OperatorStep({
  uid,
  type,
  selected,
  onPick,
}: {
  uid: string;
  type: SiftFieldType;
  selected: SiftOperator | null;
  onPick: (op: SiftOperator) => void;
}) {
  const ops = OPS[type];
  const [active, setActive] = React.useState(() => Math.max(0, ops.findIndex((o) => o.id === selected)));
  const list = useFocusOnMount<HTMLUListElement>();
  const listId = `${uid}-ops`;
  return (
    <div className="p-1.5">
      <ul
        ref={list}
        id={listId}
        role="listbox"
        tabIndex={0}
        aria-label="Operator"
        aria-activedescendant={`${listId}-${ops[active]?.id}`}
        onKeyDown={(e) => listKeys(e, ops.length, active, setActive, (i) => onPick(ops[i].id))}
        className="rounded-lg outline-none focus-visible:ring-1 focus-visible:ring-white/10"
      >
        {ops.map((o, i) => (
          <li
            key={o.id}
            id={`${listId}-${o.id}`}
            role="option"
            aria-selected={i === active}
            onPointerMove={() => setActive(i)}
            onClick={() => onPick(o.id)}
            className={cn(
              "flex h-8 cursor-pointer items-center gap-2.5 rounded-md px-2.5 text-[13px] text-white/75",
              i === active && "bg-white/[0.06] text-white"
            )}
          >
            <span className={cn("min-w-0 flex-1 truncate", o.label.length === 1 && "font-mono text-[13px]")}>
              {o.label}
              {o.hint && <span className="ml-2 font-sans text-[12px] text-white/35">{o.hint}</span>}
            </span>
            {o.id === selected && <span className="text-[var(--sift-accent)]">{Icon.check}</span>}
          </li>
        ))}
      </ul>
    </div>
  );
}

/* ---------------------------------------------------------------- value editors */

const INPUT =
  "h-9 w-full min-w-0 rounded-lg border border-white/[0.08] bg-white/[0.03] px-2.5 text-[13px] text-white tabular-nums placeholder:text-white/30 outline-none transition-colors focus:border-[color-mix(in_srgb,var(--sift-accent)_45%,transparent)]";

function TextEditor({ draft, set, submit }: { draft: Draft; set: (p: Partial<Draft>) => void; submit: () => void }) {
  const ref = useFocusOnMount<HTMLInputElement>();
  return (
    <div className="p-1.5">
      <input
        ref={ref}
        value={draft.text}
        onChange={(e) => set({ text: e.target.value })}
        onKeyDown={(e) => {
          if (e.key === "Enter") {
            e.preventDefault();
            submit();
          }
        }}
        aria-label="Value"
        placeholder="Type a value…"
        className={INPUT}
      />
    </div>
  );
}

function SelectEditor({
  uid,
  field,
  draft,
  set,
  submit,
}: {
  uid: string;
  field: SiftField;
  draft: Draft;
  set: (p: Partial<Draft>) => void;
  submit: () => void;
}) {
  const opts = React.useMemo(() => optionsOf(field), [field]);
  const [q, setQ] = React.useState("");
  const [active, setActive] = React.useState(0);
  const input = useFocusOnMount<HTMLInputElement>();
  const shown = React.useMemo(() => {
    const s = q.trim().toLowerCase();
    return s ? opts.filter((o) => o.label.toLowerCase().includes(s)) : opts;
  }, [opts, q]);
  const act = Math.min(active, shown.length - 1);
  const toggle = (v: string) =>
    set({ sel: draft.sel.includes(v) ? draft.sel.filter((x) => x !== v) : [...draft.sel, v] });
  const listId = `${uid}-opts`;
  return (
    <div className="p-1.5">
      <label className="relative flex items-center">
        <span className="pointer-events-none absolute left-2.5 text-white/35">{Icon.search}</span>
        <input
          ref={input}
          value={q}
          onChange={(e) => {
            setQ(e.target.value);
            setActive(0);
          }}
          onKeyDown={(e) => {
            if (e.key === " " && !q) {
              e.preventDefault();
              if (shown[act]) toggle(shown[act].value);
              return;
            }
            if (e.key === "Enter") {
              e.preventDefault();
              // Enter toggles while searching an unselected option; otherwise applies.
              if (q && shown[act] && !draft.sel.includes(shown[act].value)) {
                toggle(shown[act].value);
                setQ("");
              } else submit();
              return;
            }
            listKeys(e, shown.length, act, setActive, () => {});
          }}
          role="combobox"
          aria-expanded
          aria-controls={listId}
          aria-activedescendant={shown[act] ? `${listId}-${act}` : undefined}
          aria-label={`Search ${field.label.toLowerCase()} (Space toggles, Enter applies)`}
          placeholder={`Search ${field.label.toLowerCase()}…`}
          className={cn(INPUT, "pl-8")}
        />
      </label>
      <ul
        id={listId}
        role="listbox"
        aria-multiselectable
        aria-label={field.label}
        className="mt-1.5 max-h-56 overflow-y-auto"
      >
        {shown.map((o, i) => {
          const on = draft.sel.includes(o.value);
          return (
            <li
              key={o.value}
              id={`${listId}-${i}`}
              role="option"
              aria-selected={on}
              onPointerMove={() => setActive(i)}
              onClick={() => toggle(o.value)}
              className={cn(
                "flex h-8 cursor-pointer items-center gap-2.5 rounded-md px-2.5 text-[13px] text-white/75",
                i === act && "bg-white/[0.06] text-white"
              )}
            >
              <span
                aria-hidden
                className={cn(
                  "grid size-4 shrink-0 place-items-center rounded-[5px] border transition-colors duration-150",
                  on ? "border-transparent" : "border-white/20 bg-white/[0.02]"
                )}
                style={on ? { background: "var(--sift-accent)", color: "var(--sift-ink)" } : undefined}
              >
                {on && (
                  <svg viewBox="0 0 16 16" className="size-3" fill="none" stroke="currentColor" strokeWidth="2.25" aria-hidden>
                    <path d="m3.5 8.5 3 3 6-7" strokeLinecap="round" strokeLinejoin="round" />
                  </svg>
                )}
              </span>
              <Dot color={o.color} />
              <span className="flex-1 truncate">{o.label}</span>
            </li>
          );
        })}
        {!shown.length && <li className="px-2.5 py-3 text-[12.5px] text-white/40">No matches</li>}
      </ul>
    </div>
  );
}

function NumberEditor({ draft, set, submit }: { draft: Draft; set: (p: Partial<Draft>) => void; submit: () => void }) {
  const ref = useFocusOnMount<HTMLInputElement>();
  const between = draft.operator === "between";
  const onKey = (e: React.KeyboardEvent) => {
    if (e.key === "Enter") {
      e.preventDefault();
      submit();
    }
  };
  const bad = (s: string) => s.trim() !== "" && !isNum(s);
  return (
    <div className="flex items-center gap-2 p-1.5">
      <input
        ref={ref}
        inputMode="decimal"
        value={draft.numA}
        onChange={(e) => set({ numA: e.target.value })}
        onKeyDown={onKey}
        aria-label={between ? "Minimum" : "Value"}
        aria-invalid={bad(draft.numA) || undefined}
        placeholder={between ? "Min" : "0"}
        className={cn(INPUT, bad(draft.numA) && "border-red-400/50")}
      />
      {between && (
        <>
          <span className="shrink-0 text-[12px] text-white/40">and</span>
          <input
            inputMode="decimal"
            value={draft.numB}
            onChange={(e) => set({ numB: e.target.value })}
            onKeyDown={onKey}
            aria-label="Maximum"
            aria-invalid={bad(draft.numB) || undefined}
            placeholder="Max"
            className={cn(INPUT, bad(draft.numB) && "border-red-400/50")}
          />
        </>
      )}
    </div>
  );
}

function monthGrid(month: string): string[] {
  const first = fromKey(`${month}-01`);
  const offset = (first.getDay() + 6) % 7;
  const start = addDays(`${month}-01`, -offset);
  return Array.from({ length: 42 }, (_, i) => addDays(start, i));
}

function Calendar({
  range,
  a,
  b,
  today,
  onSelect,
  submit,
}: {
  range: boolean;
  a: string | null;
  b: string | null;
  today: string;
  onSelect: (a: string, b: string | null) => void;
  submit: () => void;
}) {
  const init = a ?? today;
  const [month, setMonth] = React.useState(init.slice(0, 7));
  const [focusKey, setFocusKey] = React.useState(init);
  const [hover, setHover] = React.useState<string | null>(null);
  const [kbd, setKbd] = React.useState(0);
  const grid = React.useRef<HTMLDivElement>(null);
  const days = React.useMemo(() => monthGrid(month), [month]);
  const [y, m] = month.split("-").map(Number);

  React.useEffect(() => {
    if (!kbd) return;
    grid.current?.querySelector<HTMLButtonElement>(`[data-day="${focusKey}"]`)?.focus({ preventScroll: true });
  }, [kbd, focusKey]);

  const lo = a && range ? [a, b ?? hover ?? a].sort()[0] : a;
  const hi = a && range ? [a, b ?? hover ?? a].sort()[1] : a;

  const pick = (k: string) => {
    if (!range) return onSelect(k, null);
    if (!a || b) return onSelect(k, null);
    return onSelect(k < a ? k : a, k < a ? a : k);
  };

  const move = (n: number) => {
    const next = addDays(focusKey, n);
    setFocusKey(next);
    if (next.slice(0, 7) !== month) setMonth(next.slice(0, 7));
    setKbd((x) => x + 1);
  };

  return (
    <div className="px-2 pb-1 pt-0.5">
      <div className="flex items-center justify-between px-0.5 pb-1">
        <span className="text-[12.5px] font-medium text-white/85" aria-live="polite">
          {MONTHS_LONG[m - 1]} {y}
        </span>
        <div className="flex gap-0.5">
          {[
            { d: -1, icon: Icon.chevL, label: "Previous month" },
            { d: 1, icon: Icon.chevR, label: "Next month" },
          ].map((x) => (
            <button
              key={x.d}
              type="button"
              aria-label={x.label}
              onClick={() => {
                const nm = addMonths(month, x.d);
                setMonth(nm);
                setFocusKey(`${nm}-01`);
              }}
              className={cn("grid size-7 place-items-center rounded-md text-white/50 hover:bg-white/[0.06] hover:text-white", FOCUS)}
            >
              {x.icon}
            </button>
          ))}
        </div>
      </div>
      <div
        ref={grid}
        role="grid"
        aria-label={`${MONTHS_LONG[m - 1]} ${y}${range ? ", pick a start and end date" : ""}`}
        onPointerLeave={() => setHover(null)}
        onKeyDown={(e) => {
          const map: Record<string, number> = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7 };
          if (map[e.key] !== undefined) {
            e.preventDefault();
            move(map[e.key]);
          } else if (e.key === "PageUp" || e.key === "PageDown") {
            e.preventDefault();
            move(e.key === "PageUp" ? -28 : 28);
          } else if (e.key === "Enter" && a && (!range || b) && focusKey === (range ? b : a)) {
            e.preventDefault();
            submit();
          }
        }}
      >
        <div role="row" className="grid grid-cols-7">
          {WEEKDAYS.map((w) => (
            <span key={w} role="columnheader" className="grid h-7 place-items-center text-[11px] text-white/35">
              {w}
            </span>
          ))}
        </div>
        {[0, 1, 2, 3, 4, 5].map((r) => (
          <div role="row" key={r} className="grid grid-cols-7">
            {days.slice(r * 7, r * 7 + 7).map((k) => {
              const out = k.slice(0, 7) !== month;
              const isEnd = k === lo || k === hi;
              const inside = !!lo && !!hi && k > lo && k < hi;
              const d = fromKey(k);
              return (
                <div role="gridcell" key={k} aria-selected={isEnd || inside} className="relative h-8 p-px">
                  {(inside || (isEnd && lo !== hi)) && (
                    <span
                      aria-hidden
                      className={cn(
                        "absolute inset-y-px bg-[color-mix(in_srgb,var(--sift-accent)_12%,transparent)]",
                        k === lo ? "left-1/2 right-0" : k === hi ? "left-0 right-1/2" : "inset-x-0"
                      )}
                    />
                  )}
                  <button
                    type="button"
                    data-day={k}
                    tabIndex={k === focusKey ? 0 : -1}
                    aria-label={`${MONTHS_LONG[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}${k === today ? " (today)" : ""}`}
                    onClick={() => {
                      setFocusKey(k);
                      pick(k);
                    }}
                    onPointerEnter={() => setHover(k)}
                    className={cn(
                      "relative grid h-full w-full place-items-center rounded-md text-[12.5px] tabular-nums transition-colors",
                      FOCUS,
                      out ? "text-white/25" : "text-white/75",
                      !isEnd && "hover:bg-white/[0.07] hover:text-white",
                      isEnd && "font-semibold"
                    )}
                    style={isEnd ? { background: "var(--sift-accent)", color: "var(--sift-ink)" } : undefined}
                  >
                    {d.getDate()}
                    {k === today && (
                      <span
                        aria-hidden
                        className="absolute bottom-1 left-1/2 size-[3px] -translate-x-1/2 rounded-full"
                        style={{ background: isEnd ? "var(--sift-ink)" : "var(--sift-accent)" }}
                      />
                    )}
                  </button>
                </div>
              );
            })}
          </div>
        ))}
      </div>
    </div>
  );
}

function DateEditor({
  draft,
  set,
  today,
  submit,
}: {
  draft: Draft;
  set: (p: Partial<Draft>) => void;
  today: string;
  submit: () => void;
}) {
  const first = useFocusOnMount<HTMLDivElement>();
  return (
    <div>
      <div ref={first} tabIndex={-1} className="flex flex-wrap gap-1 px-2 pb-1.5 pt-1.5 outline-none" role="group" aria-label="Presets">
        {PRESETS.map((p) => {
          const on = draft.preset === p.id;
          return (
            <button
              key={p.id}
              type="button"
              aria-pressed={on}
              onClick={() => set({ preset: on ? null : p.id, dateA: null, dateB: null })}
              className={cn(
                "h-7 rounded-md border px-2 text-[12px] transition-colors",
                FOCUS,
                on
                  ? "border-[color-mix(in_srgb,var(--sift-accent)_45%,transparent)] bg-[color-mix(in_srgb,var(--sift-accent)_14%,transparent)] text-white"
                  : "border-white/[0.08] text-white/60 hover:border-white/15 hover:text-white"
              )}
            >
              {p.label}
            </button>
          );
        })}
      </div>
      <div className="mx-2 h-px bg-white/[0.06]" />
      <Calendar
        range={draft.operator === "between"}
        a={draft.preset ? null : draft.dateA}
        b={draft.preset ? null : draft.dateB}
        today={today}
        onSelect={(a, b) => set({ dateA: a, dateB: b, preset: null })}
        submit={submit}
      />
    </div>
  );
}

/* ================================================================ views menu */

function ViewsMenu({
  uid,
  views,
  activeId,
  edited,
  open,
  setOpen,
  onPick,
}: {
  uid: string;
  views: SiftView[];
  activeId: string | null;
  edited: boolean;
  open: boolean;
  setOpen: (o: boolean) => void;
  onPick: (v: SiftView) => void;
}) {
  const btn = React.useRef<HTMLButtonElement>(null);
  const menu = React.useRef<HTMLDivElement>(null);
  const reduced = useReducedMotion();
  const active = views.find((v) => v.id === activeId);
  const menuId = `${uid}-views`;

  React.useEffect(() => {
    if (!open) return;
    const idx = Math.max(0, views.findIndex((v) => v.id === activeId));
    const id = requestAnimationFrame(() =>
      menu.current?.querySelectorAll<HTMLElement>("[role=menuitemradio]")[idx]?.focus({ preventScroll: true })
    );
    const onDown = (e: PointerEvent) => {
      if (!menu.current?.contains(e.target as Node) && !btn.current?.contains(e.target as Node)) setOpen(false);
    };
    document.addEventListener("pointerdown", onDown);
    return () => {
      cancelAnimationFrame(id);
      document.removeEventListener("pointerdown", onDown);
    };
  }, [open, views, activeId, setOpen]);

  const close = () => {
    setOpen(false);
    btn.current?.focus({ preventScroll: true });
  };

  return (
    <div className="relative min-w-0">
      <button
        ref={btn}
        type="button"
        aria-haspopup="menu"
        aria-expanded={open}
        aria-controls={open ? menuId : undefined}
        onClick={() => setOpen(!open)}
        onKeyDown={(e) => {
          if (e.key === "ArrowDown" && !open) {
            e.preventDefault();
            setOpen(true);
          }
        }}
        className={cn(
          "flex h-8 min-w-0 max-w-full items-center gap-2 rounded-lg px-2 text-[13px] font-medium text-white/90 transition-colors hover:bg-white/[0.05]",
          open && "bg-white/[0.05]",
          FOCUS
        )}
      >
        <span className="text-white/45">{Icon.views}</span>
        <span className="truncate">{active?.label ?? "Custom view"}</span>
        {edited && (
          <span className="shrink-0 rounded-[5px] border border-white/10 px-1 py-px text-[10.5px] font-normal leading-tight text-white/50">
            Edited
          </span>
        )}
        <span className={cn("text-white/40 transition-transform duration-200", open && "rotate-180")}>{Icon.chevron}</span>
      </button>
      <AnimatePresence>
        {open && (
          <motion.div
            ref={menu}
            id={menuId}
            role="menu"
            aria-label="Saved views"
            initial={reduced ? { opacity: 0 } : { opacity: 0, y: -4, scale: 0.98 }}
            animate={{ opacity: 1, y: 0, scale: 1 }}
            exit={reduced ? { opacity: 0 } : { opacity: 0, y: -4, scale: 0.98 }}
            transition={reduced ? FADE : { duration: 0.16, ease: EASE }}
            style={{ transformOrigin: "top left" }}
            onKeyDown={(e) => {
              const items = [...(menu.current?.querySelectorAll<HTMLElement>("[role=menuitemradio]") ?? [])];
              const i = items.indexOf(document.activeElement as HTMLElement);
              if (e.key === "ArrowDown" || e.key === "ArrowUp") {
                e.preventDefault();
                const n = (i + (e.key === "ArrowDown" ? 1 : -1) + items.length) % items.length;
                items[n]?.focus({ preventScroll: true });
              } else if (e.key === "Home" || e.key === "End") {
                e.preventDefault();
                items[e.key === "Home" ? 0 : items.length - 1]?.focus({ preventScroll: true });
              } else if (e.key === "Escape" || e.key === "Tab") {
                e.preventDefault();
                close();
              }
            }}
            className="absolute left-0 top-[calc(100%+6px)] z-40 w-60 max-w-[calc(100cqw-1rem)] rounded-xl border border-white/10 bg-[#141416] p-1 shadow-[0_24px_48px_-12px_rgba(0,0,0,0.75),0_0_0_1px_rgba(0,0,0,0.4)]"
          >
            <p className="px-2.5 pb-1 pt-1.5 text-[11px] font-medium uppercase tracking-[0.08em] text-white/35">Views</p>
            {views.map((v) => {
              const on = v.id === activeId;
              return (
                <div
                  key={v.id}
                  role="menuitemradio"
                  aria-checked={on}
                  tabIndex={-1}
                  onClick={() => {
                    onPick(v);
                    close();
                  }}
                  onKeyDown={(e) => {
                    if (e.key === "Enter" || e.key === " ") {
                      e.preventDefault();
                      onPick(v);
                      close();
                    }
                  }}
                  className="flex h-8 cursor-pointer items-center gap-2.5 rounded-md px-2.5 text-[13px] text-white/75 outline-none hover:bg-white/[0.06] hover:text-white focus-visible:bg-white/[0.06] focus-visible:text-white"
                >
                  <span className="grid w-3.5 place-items-center text-[var(--sift-accent)]">{on && Icon.check}</span>
                  <span className="flex-1 truncate">{v.label}</span>
                  <span className="text-[11.5px] tabular-nums text-white/30">{v.filters.length || "—"}</span>
                </div>
              );
            })}
          </motion.div>
        )}
      </AnimatePresence>
    </div>
  );
}

/* ================================================================ main */

/**
 * Sift — a structured filter builder for any list or table. "+ Filter" opens
 * one popover that slides property → operator → value; applied filters become
 * editable chips joined by an and/or toggle; saved views swap chip sets with a
 * crossfade. Pair it with the exported `applyFilters(rows, filters, fields)`.
 */
export function Sift({
  fields: fieldsProp,
  value,
  defaultValue,
  onChange,
  conjunction: conjProp,
  defaultConjunction = "and",
  onConjunctionChange,
  views: viewsProp,
  defaultView,
  onViewChange,
  onSaveView,
  count,
  total,
  noun = { one: "result", other: "results" },
  today: todayProp,
  accentColor = DEFAULT_ACCENT,
  ref,
  className,
  style,
  onKeyDown,
  ...rest
}: SiftProps) {
  const reduced = useReducedMotion();
  const uid = React.useId().replace(/:/g, "");
  const sample = !fieldsProp;
  const fields = fieldsProp ?? defaultSiftFields;
  const baseViews = React.useMemo(() => viewsProp ?? (sample ? defaultSiftViews : []), [viewsProp, sample]);
  const initialViewId = defaultView ?? (sample && !viewsProp ? "active" : baseViews[0]?.id) ?? null;

  /* ----- state */
  const [innerFilters, setInnerFilters] = React.useState<SiftFilter[]>(
    () => defaultValue ?? (sample ? (baseViews.find((v) => v.id === initialViewId)?.filters ?? []) : [])
  );
  const filters = value ?? innerFilters;
  const [innerConj, setInnerConj] = React.useState<SiftConjunction>(defaultConjunction);
  const conj = conjProp ?? innerConj;
  const [saved, setSaved] = React.useState<SiftView[]>([]);
  const views = React.useMemo(() => [...baseViews, ...saved], [baseViews, saved]);
  const [activeViewId, setActiveViewId] = React.useState<string | null>(initialViewId);
  const activeView = views.find((v) => v.id === activeViewId);
  const edited = activeView
    ? sig(activeView.filters, activeView.conjunction ?? "and") !== sig(filters, conj)
    : filters.length > 0;
  const [epoch, setEpoch] = React.useState(0);
  const [announce, setAnnounce] = React.useState("");
  const idSeq = React.useRef(0);
  const freshId = (prefix: string, taken: string[]) => {
    let id = "";
    do id = `${prefix}-${uid}-${(idSeq.current++).toString(36)}`;
    while (taken.includes(id));
    return id;
  };
  const todayKey = useTodayKey(todayProp) ?? "2026-01-01";

  const [pop, setPop] = React.useState<{ anchor: string; step: Step; dir: 1 | -1 } | null>(null);
  const [popPos, setPopPos] = React.useState<{ left: number; top: number; width: number } | null>(null);
  const [draft, setDraft] = React.useState<Draft>(EMPTY_DRAFT);
  const [viewsOpen, setViewsOpen] = React.useState(false);

  const [saving, setSaving] = React.useState<null | { name: string; status: "idle" | "pending" | "error"; error?: string }>(
    null
  );

  const rootRef = React.useRef<HTMLDivElement>(null);
  const addRef = React.useRef<HTMLButtonElement>(null);
  const popRef = React.useRef<HTMLDivElement>(null);
  const anchorEl = React.useRef<HTMLElement | null>(null);
  const restoreEl = React.useRef<HTMLElement | null>(null);
  const saveInput = React.useRef<HTMLInputElement>(null);

  /* ----- intro (once in view; falls back on a timer so it never stays hidden) */
  const inView = useInView(rootRef, { once: true, amount: 0.2 });
  const [forced, setForced] = React.useState(false);
  const [introKey, setIntroKey] = React.useState(0);
  React.useEffect(() => {
    const t = window.setTimeout(() => setForced(true), 1200);
    return () => window.clearTimeout(t);
  }, []);
  const play = reduced || inView || forced;
  const [introDone, setIntroDone] = React.useState(false);
  React.useEffect(() => {
    if (!play || introDone) return;
    const t = window.setTimeout(() => setIntroDone(true), 900);
    return () => window.clearTimeout(t);
  }, [play, introDone, introKey]);

  /* ----- commits */
  const commit = React.useCallback(
    (next: SiftFilter[], msg?: string, keepView = true) => {
      if (value === undefined) setInnerFilters(next);
      onChange?.(next);
      if (msg) setAnnounce(msg);
      if (!keepView) setActiveViewId(null);
    },
    [value, onChange]
  );

  const setConj = (c: SiftConjunction) => {
    if (conjProp === undefined) setInnerConj(c);
    onConjunctionChange?.(c);
    setAnnounce(c === "and" ? "Matching all filters" : "Matching any filter");
  };

  const fieldOf = (key: string | null) => fields.find((f) => f.key === key);

  /* ----- popover */
  const place = React.useCallback(() => {
    const root = rootRef.current;
    const a = anchorEl.current;
    if (!root || !a) return;
    const r = root.getBoundingClientRect();
    const ar = a.getBoundingClientRect();
    const width = Math.min(POP_W, r.width - 12);
    const left = Math.max(6, Math.min(ar.left - r.left, r.width - width - 6));
    setPopPos({ left, top: ar.bottom - r.top + 6, width });
  }, []);

  const openAt = (anchor: string, el: HTMLElement | null, step: Step, d: Draft) => {
    setViewsOpen(false);
    setSaving(null);
    anchorEl.current = el;
    restoreEl.current = (document.activeElement as HTMLElement) ?? el;
    setDraft(d);
    setPop({ anchor, step, dir: 1 });
    place();
  };

  const openAdd = (fieldKey?: string) => {
    const f = fieldKey ? fieldOf(fieldKey) : undefined;
    openAt("add", addRef.current, f ? "operator" : "field", { ...EMPTY_DRAFT, field: f?.key ?? null });
  };

  const openEdit = (filter: SiftFilter, step: Step, el: HTMLElement | null) => {
    if (pop?.anchor === filter.id && pop.step === step) return closePop(true);
    openAt(filter.id, el, step, draftFrom(filter));
  };

  const closePop = (restore: boolean) => {
    setPop(null);
    if (restore) {
      const el = restoreEl.current;
      requestAnimationFrame(() => (el?.isConnected ? el : addRef.current)?.focus({ preventScroll: true }));
    }
  };

  const go = (step: Step) => setPop((p) => (p ? { ...p, step, dir: STEP_INDEX[step] >= STEP_INDEX[p.step] ? 1 : -1 } : p));

  const apply = (d: Draft = draft) => {
    const field = fieldOf(d.field);
    if (!field) return;
    const id = d.editingId ?? freshId("f", filters.map((x) => x.id));
    const f = buildFilter(d, field, id);
    if (!f) return;
    if (d.editingId) {
      commit(
        filters.map((x) => (x.id === d.editingId ? f : x)),
        `Filter updated: ${describe(field, f)}`
      );
    } else {
      commit([...filters, f], `Filter added: ${describe(field, f)}`);
      restoreEl.current = addRef.current;
    }
    closePop(true);
  };

  const pickField = (key: string) => {
    const nf = fieldOf(key);
    if (!nf) return;
    const prev = fieldOf(draft.field);
    const keep = prev && prev.type === nf.type && prev.key === nf.key;
    const sameType = prev && prev.type === nf.type;
    setDraft((d) => ({
      ...(keep ? d : sameType && nf.type !== "select" ? d : { ...EMPTY_DRAFT, editingId: d.editingId }),
      field: key,
    }));
    go("operator");
  };

  const pickOperator = (op: SiftOperator) => {
    const field = fieldOf(draft.field);
    if (!field) return;
    let d: Draft = { ...draft, operator: op };
    if (field.type === "date" && op !== "is") d = { ...d, preset: null };
    if (field.type === "date" && op !== "between") d = { ...d, dateB: null };
    if (field.type === "boolean") return apply(d);
    setDraft(d);
    // Editing in place: an operator swap that keeps a valid value applies at once.
    if (draft.editingId && buildFilter(d, field, draft.editingId) && pop?.step === "operator" && draft.operator) {
      const prevBetween = draft.operator === "between";
      if (prevBetween === (op === "between")) return apply(d);
    }
    go("value");
  };

  React.useEffect(() => {
    if (!pop) return;
    const root = rootRef.current;
    const ro = new ResizeObserver(() => place());
    if (root) ro.observe(root);
    const onDown = (e: PointerEvent) => {
      const t = e.target as Node;
      if (popRef.current?.contains(t) || anchorEl.current?.contains(t)) return;
      setPop(null);
    };
    document.addEventListener("pointerdown", onDown);
    return () => {
      ro.disconnect();
      document.removeEventListener("pointerdown", onDown);
    };
  }, [pop, place]);

  /* ----- chip ops */
  const removeChip = (id: string, focusAfter = false) => {
    const i = filters.findIndex((x) => x.id === id);
    if (i < 0) return;
    const f = filters[i];
    const field = fieldOf(f.field);
    const next = filters.filter((x) => x.id !== id);
    commit(next, field ? `Removed ${describe(field, f)}` : "Filter removed");
    if (pop?.anchor === id) setPop(null);
    if (focusAfter) {
      const target = next[Math.max(0, i - 1)]?.id;
      requestAnimationFrame(() => {
        const el = target
          ? rootRef.current?.querySelector<HTMLElement>(`[data-chip="${target}"] [data-part="field"]`)
          : addRef.current;
        el?.focus({ preventScroll: true });
      });
    }
  };

  const clearAll = () => {
    if (!filters.length) return;
    commit([], "All filters cleared");
    setPop(null);
    requestAnimationFrame(() => addRef.current?.focus({ preventScroll: true }));
  };

  const pickView = (v: SiftView) => {
    setActiveViewId(v.id);
    setEpoch((e) => e + 1);
    commit(v.filters, `View: ${v.label}, ${v.filters.length} filter${v.filters.length === 1 ? "" : "s"}`);
    const c = v.conjunction ?? "and";
    if (c !== conj) {
      if (conjProp === undefined) setInnerConj(c);
      onConjunctionChange?.(c);
    }
    setPop(null);
    onViewChange?.(v);
  };

  const startSave = () => {
    setPop(null);
    setViewsOpen(false);
    setSaving({ name: "", status: "idle" });
    requestAnimationFrame(() => saveInput.current?.focus({ preventScroll: true }));
  };

  const submitSave = async () => {
    if (!saving || saving.status === "pending") return;
    const name = saving.name.trim();
    if (!name) {
      setSaving({ ...saving, status: "error", error: "Name the view" });
      return;
    }
    const view: SiftView = {
      id: freshId("view", views.map((v) => v.id)),
      label: name,
      filters: filters.map((f) => ({ ...f })),
      conjunction: conj,
    };
    try {
      const r = onSaveView?.(view);
      if (r && typeof (r as Promise<unknown>).then === "function") {
        setSaving({ name, status: "pending" });
        await r;
      }
      setSaved((s) => [...s, view]);
      setActiveViewId(view.id);
      setSaving(null);
      setAnnounce(`View saved: ${name}`);
    } catch (err) {
      setSaving({ name, status: "error", error: err instanceof Error ? err.message : "Couldn't save view" });
      requestAnimationFrame(() => saveInput.current?.focus({ preventScroll: true }));
    }
  };

  /* ----- handle */
  React.useImperativeHandle(
    ref,
    () => ({
      open: (k?: string) => openAdd(k),
      close: () => {
        setPop(null);
        setViewsOpen(false);
      },
      clear: () => commit([], "All filters cleared"),
      replay: () => {
        setIntroDone(false);
        setIntroKey((k) => k + 1);
      },
    })
  );

  /* ----- keyboard */
  const onRootKey = (e: React.KeyboardEvent<HTMLDivElement>) => {
    onKeyDown?.(e);
    if (e.defaultPrevented) return;
    const t = e.target as HTMLElement;
    const typing = t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable;
    if (e.key === "Escape" && pop) {
      e.preventDefault();
      closePop(true);
      return;
    }
    if (pop && e.key === "Tab" && popRef.current?.contains(t)) {
      // Trap focus within the popover.
      const items = [
        ...popRef.current.querySelectorAll<HTMLElement>(
          'button:not([disabled]), input, [tabindex="0"]'
        ),
      ].filter((el) => el.offsetParent !== null);
      if (items.length) {
        const i = items.indexOf(t);
        const n = e.shiftKey ? (i <= 0 ? items.length - 1 : i - 1) : i === items.length - 1 ? 0 : i + 1;
        e.preventDefault();
        items[n].focus({ preventScroll: true });
      }
      return;
    }
    if (!typing && !pop && e.key.toLowerCase() === "f" && !e.metaKey && !e.ctrlKey && !e.altKey) {
      e.preventDefault();
      addRef.current?.focus({ preventScroll: true });
    }
  };

  const onChipKey = (e: React.KeyboardEvent, i: number, id: string) => {
    if (e.key === "Backspace" || e.key === "Delete") {
      e.preventDefault();
      e.stopPropagation();
      removeChip(id, true);
    } else if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
      e.preventDefault();
      const n = i + (e.key === "ArrowRight" ? 1 : -1);
      if (n < 0) return;
      const target = filters[n]?.id;
      const el = target
        ? rootRef.current?.querySelector<HTMLElement>(`[data-chip="${target}"] [data-part="field"]`)
        : addRef.current;
      el?.focus({ preventScroll: true });
    }
  };

  /* ----- derived render bits */
  const accentStyle = {
    "--sift-accent": accentColor,
    "--sift-ink": inkFor(accentColor),
    ...style,
  } as React.CSSProperties;
  const popField = fieldOf(draft.field);
  const popOp = draft.operator && popField ? opLabel(popField.type, draft.operator) : null;
  const canApply = !!(popField && buildFilter(draft, popField, "x"));
  const showViews = views.length > 0;
  const prevCount = usePrev(count);
  const layoutOn = !reduced;

  const chipTransition = (i: number): Transition =>
    reduced ? REDUCED_T : { ...SPRING, delay: introDone ? 0 : 0.14 + i * 0.05 };

  /* ----- step body */
  let body: React.ReactNode = null;
  if (pop) {
    if (pop.step === "field") body = <FieldStep uid={uid} fields={fields} selected={draft.field} onPick={pickField} />;
    else if (pop.step === "operator" && popField)
      body = <OperatorStep uid={uid} type={popField.type} selected={draft.operator} onPick={pickOperator} />;
    else if (pop.step === "value" && popField) {
      const set = (p: Partial<Draft>) => setDraft((d) => ({ ...d, ...p }));
      const submit = () => apply();
      body =
        popField.type === "text" ? (
          <TextEditor draft={draft} set={set} submit={submit} />
        ) : popField.type === "select" ? (
          <SelectEditor uid={uid} field={popField} draft={draft} set={set} submit={submit} />
        ) : popField.type === "number" ? (
          <NumberEditor draft={draft} set={set} submit={submit} />
        ) : popField.type === "date" ? (
          <DateEditor draft={draft} set={set} today={todayKey} submit={submit} />
        ) : null;
    }
  }

  return (
    <div
      ref={rootRef}
      data-crucible="sift"
      className={cn("@container relative w-full text-[13px] text-white", className)}
      style={accentStyle}
      onKeyDown={onRootKey}
      {...rest}
    >
      <motion.div
        key={introKey}
        initial={{ opacity: 0, y: 8 }}
        animate={play ? { opacity: 1, y: 0 } : undefined}
        transition={reduced ? { duration: 0.15, ease: "easeOut" } : { duration: 0.45, ease: EASE }}
        className="rounded-2xl border border-white/[0.08] bg-[#111113]/90 shadow-[inset_0_1px_0_rgba(255,255,255,0.04),0_24px_60px_-28px_rgba(0,0,0,0.8)] backdrop-blur-sm"
      >
        {/* header: views · count · save */}
        <div className="flex min-h-12 flex-wrap items-center gap-x-2 gap-y-1 border-b border-white/[0.06] px-2 py-1.5">
          {showViews ? (
            <ViewsMenu
              uid={uid}
              views={views}
              activeId={activeViewId}
              edited={edited}
              open={viewsOpen}
              setOpen={(o) => {
                if (o) setPop(null);
                setViewsOpen(o);
              }}
              onPick={pickView}
            />
          ) : (
            <span className="flex h-8 items-center gap-2 px-2 text-[13px] font-medium text-white/85">
              <span className="text-white/45">{Icon.funnel}</span>Filters
            </span>
          )}

          <div className="ml-auto flex min-w-0 items-center gap-2.5">
            {count !== undefined && (
              <span className="flex items-baseline gap-1 whitespace-nowrap px-1 text-[12.5px] text-white/45" aria-live="polite">
                {reduced ? (
                  <span className="font-medium tabular-nums text-white">{count}</span>
                ) : (
                  <Tally
                    key={count}
                    value={count}
                    start={prevCount ?? count}
                    duration={0.55}
                    stagger={0.03}
                    glow={false}
                    glint={false}
                    className="font-medium text-white"
                  />
                )}
                {total !== undefined && <span className="tabular-nums">of {total}</span>}
                <span>{count === 1 ? noun.one : noun.other}</span>
              </span>
            )}
            {onSaveView && (
              <AnimatePresence mode="popLayout" initial={false}>
                {saving ? (
                  <motion.form
                    key="save-form"
                    initial={reduced ? { opacity: 0 } : { opacity: 0, width: 96 }}
                    animate={{ opacity: 1, width: "auto" }}
                    exit={{ opacity: 0 }}
                    transition={reduced ? FADE : { duration: 0.22, ease: EASE }}
                    onSubmit={(e) => {
                      e.preventDefault();
                      void submitSave();
                    }}
                    className="flex items-center gap-1"
                  >
                    <motion.input
                      ref={saveInput}
                      value={saving.name}
                      disabled={saving.status === "pending"}
                      onChange={(e) => setSaving({ name: e.target.value, status: "idle" })}
                      onKeyDown={(e) => {
                        if (e.key === "Escape") {
                          e.preventDefault();
                          e.stopPropagation();
                          setSaving(null);
                        }
                      }}
                      onBlur={(e) => {
                        if (!saving.name.trim() && !e.currentTarget.form?.contains(e.relatedTarget as Node)) setSaving(null);
                      }}
                      animate={saving.status === "error" && !reduced ? { x: [0, -4, 4, -2, 0] } : { x: 0 }}
                      transition={{ duration: 0.3 }}
                      aria-label="View name"
                      aria-invalid={saving.status === "error" || undefined}
                      title={saving.error}
                      placeholder={saving.error ?? "View name"}
                      maxLength={40}
                      className={cn(
                        "h-8 w-[min(11rem,40cqw)] rounded-lg border bg-white/[0.04] px-2.5 text-[13px] text-white placeholder:text-white/35 outline-none transition-colors focus:border-[color-mix(in_srgb,var(--sift-accent)_50%,transparent)] disabled:opacity-60",
                        saving.status === "error" ? "border-red-400/50 placeholder:text-red-300/70" : "border-white/10"
                      )}
                    />
                    <button
                      type="submit"
                      aria-label="Save view"
                      disabled={saving.status === "pending"}
                      className={cn(
                        "grid size-8 shrink-0 place-items-center rounded-lg transition-[filter] hover:brightness-110 disabled:cursor-progress",
                        FOCUS
                      )}
                      style={{ background: "var(--sift-accent)", color: "var(--sift-ink)" }}
                    >
                      {saving.status === "pending" ? (
                        <motion.span
                          aria-hidden
                          className="block size-3.5 rounded-full border-2 border-current border-t-transparent"
                          animate={reduced ? undefined : { rotate: 360 }}
                          transition={{ duration: 0.8, repeat: Infinity, ease: "linear" }}
                        />
                      ) : (
                        Icon.check
                      )}
                    </button>
                  </motion.form>
                ) : (
                  <motion.button
                    key="save-btn"
                    type="button"
                    initial={{ opacity: 0 }}
                    animate={{ opacity: !filters.length || !edited ? 0.35 : 1 }}
                    exit={{ opacity: 0 }}
                    transition={FADE}
                    onClick={startSave}
                    disabled={!filters.length || !edited}
                    className={cn(
                      "h-8 whitespace-nowrap rounded-lg border border-white/10 px-2.5 text-[12.5px] font-medium text-white/75 transition-colors hover:border-white/15 hover:bg-white/[0.04] hover:text-white disabled:pointer-events-none",
                      FOCUS
                    )}
                  >
                    Save view
                  </motion.button>
                )}
              </AnimatePresence>
            )}
          </div>
        </div>

        {/* chips row — views crossfade by stacking epochs in one grid cell */}
        <div className="grid p-2">
          <AnimatePresence initial={false}>
            <motion.div
              key={epoch}
              className="relative col-start-1 row-start-1"
              initial={{ opacity: 0, filter: reduced ? "blur(0px)" : "blur(3px)" }}
              animate={{ opacity: 1, filter: "blur(0px)" }}
              exit={{ opacity: 0, filter: reduced ? "blur(0px)" : "blur(3px)" }}
              transition={{ duration: reduced ? 0.15 : 0.24, ease: "easeOut" }}
            >
              <LayoutGroup id={`${uid}-${epoch}`}>
                <div role="group" aria-label="Applied filters" className="relative flex flex-wrap items-center gap-1.5">
                  <AnimatePresence mode="popLayout" initial={true}>
                    {filters.map((f, i) => {
                      const field = fieldOf(f.field);
                      if (!field) return null;
                      const val = summarize(field, f);
                      const on = pop?.anchor === f.id;
                      const opts = field.type === "select" ? optionsOf(field) : [];
                      const firstColor =
                        field.type === "select" && Array.isArray(f.value) && f.value.length === 1
                          ? (opts.find((o) => o.value === (f.value as string[])[0])?.color ?? "")
                          : "";
                      return (
                        <motion.div
                          key={f.id}
                          layout={layoutOn ? "position" : false}
                          className="flex items-center gap-1.5"
                          initial={{ opacity: 0, scale: 0.86, y: 3 }}
                          animate={play ? { opacity: 1, scale: 1, y: 0 } : undefined}
                          exit={reduced ? { opacity: 0 } : { opacity: 0, scale: 0.86, filter: "blur(2px)" }}
                          transition={chipTransition(i)}
                        >
                          {i > 0 && (
                            <button
                              type="button"
                              onClick={() => setConj(conj === "and" ? "or" : "and")}
                              aria-label={`Combine filters with ${conj}. Switch to ${conj === "and" ? "or" : "and"}`}
                              className={cn(
                                "h-6 min-w-8 rounded-md px-1.5 text-[11.5px] font-medium uppercase tracking-[0.06em] text-white/40 transition-colors hover:bg-white/[0.05] hover:text-white/80",
                                FOCUS
                              )}
                            >
                              <AnimatePresence mode="popLayout" initial={false}>
                                <motion.span
                                  key={conj}
                                  className="inline-block"
                                  initial={reduced ? { opacity: 0 } : { opacity: 0, y: 6 }}
                                  animate={{ opacity: 1, y: 0 }}
                                  exit={reduced ? { opacity: 0 } : { opacity: 0, y: -6 }}
                                  transition={{ duration: 0.16, ease: EASE }}
                                >
                                  {conj}
                                </motion.span>
                              </AnimatePresence>
                            </button>
                          )}
                          <div
                            data-chip={f.id}
                            role="group"
                            aria-label={`Filter: ${describe(field, f)}. Backspace removes, arrow keys move between filters`}
                            onKeyDown={(e) => onChipKey(e, i, f.id)}
                            className={cn(
                              "flex h-8 max-w-full items-stretch overflow-hidden rounded-[9px] border bg-white/[0.035] transition-[border-color,box-shadow,background-color] duration-200",
                              on
                                ? "border-[color-mix(in_srgb,var(--sift-accent)_55%,transparent)] bg-[color-mix(in_srgb,var(--sift-accent)_7%,transparent)] shadow-[0_0_0_3px_color-mix(in_srgb,var(--sift-accent)_12%,transparent)]"
                                : "border-white/[0.09] hover:border-white/[0.14]"
                            )}
                          >
                            <ChipPart
                              part="field"
                              label={`Change property (${field.label})`}
                              onClick={(e) => openEdit(f, "field", e.currentTarget.closest("[data-chip]"))}
                              className="gap-1.5 pl-2.5 text-white/60"
                            >
                              <span className="text-white/40">{field.icon ?? TYPE_ICON[field.type]}</span>
                              <span className="hidden whitespace-nowrap @[18rem]:inline">{field.label}</span>
                            </ChipPart>
                            <ChipPart
                              part="op"
                              label={`Change operator (${opLabel(field.type, f.operator)})`}
                              onClick={(e) => openEdit(f, "operator", e.currentTarget.closest("[data-chip]"))}
                              className={cn("text-white/40", opLabel(field.type, f.operator).length === 1 && "font-mono")}
                            >
                              <span className="whitespace-nowrap">{opLabel(field.type, f.operator)}</span>
                            </ChipPart>
                            {val && (
                              <ChipPart
                                part="value"
                                label={`Change value (${val})`}
                                onClick={(e) => openEdit(f, "value", e.currentTarget.closest("[data-chip]"))}
                                className="min-w-0 gap-1.5 font-medium text-[var(--sift-accent)]"
                              >
                                <Dot color={firstColor} />
                                <span className="truncate tabular-nums">{val}</span>
                              </ChipPart>
                            )}
                            <button
                              type="button"
                              aria-label={`Remove filter ${describe(field, f)}`}
                              onClick={() => removeChip(f.id, true)}
                              className="grid w-7 shrink-0 place-items-center border-l border-white/[0.06] text-white/35 transition-colors hover:bg-white/[0.06] hover:text-white focus-visible:bg-white/[0.08] focus-visible:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--sift-accent)]"
                            >
                              {Icon.x}
                            </button>
                          </div>
                        </motion.div>
                      );
                    })}
                    {/* Add + Clear all wrap together, so "Clear all" never lands alone on a row. */}
                    <motion.div
                      key="tail"
                      layout={layoutOn ? "position" : false}
                      className={cn(
                        "flex flex-1 items-center gap-1.5",
                        // nowrap → the pair's min-content is both buttons, so the row
                        // breaks before them rather than between them.
                        filters.length ? "flex-nowrap" : "min-w-0 flex-wrap"
                      )}
                    >
                      <motion.button
                        ref={addRef}
                        type="button"
                        aria-haspopup="dialog"
                        aria-expanded={pop?.anchor === "add"}
                        aria-keyshortcuts="F"
                        onClick={() => (pop?.anchor === "add" ? closePop(true) : openAdd())}
                        initial={{ opacity: 0, y: 3 }}
                        animate={play ? { opacity: 1, y: 0 } : undefined}
                        transition={reduced ? REDUCED_T : { ...SPRING, delay: introDone ? 0 : 0.14 + filters.length * 0.05 }}
                        className={cn(
                          "flex h-8 items-center gap-1.5 rounded-[9px] border border-dashed px-2.5 text-[12.5px] font-medium transition-colors",
                          pop?.anchor === "add"
                            ? "border-[color-mix(in_srgb,var(--sift-accent)_55%,transparent)] bg-white/[0.05] text-white"
                            : "border-white/15 text-white/60 hover:border-white/25 hover:bg-white/[0.03] hover:text-white",
                          FOCUS
                        )}
                      >
                        {Icon.plus}
                        Filter
                        <kbd className="ml-0.5 hidden rounded border border-white/10 px-1 font-sans text-[10px] leading-[14px] text-white/35 @[30rem]:inline">
                          F
                        </kbd>
                      </motion.button>
                      <AnimatePresence initial={false} mode="popLayout">
                        {!filters.length && (
                          <motion.span
                            key="empty"
                            initial={{ opacity: 0 }}
                            animate={{ opacity: 1 }}
                            exit={{ opacity: 0 }}
                            transition={FADE}
                            className="px-1 text-[12.5px] text-white/35"
                          >
                            No filters — showing everything
                          </motion.span>
                        )}
                        {filters.length > 0 && (
                          <motion.button
                            key="clear"
                            type="button"
                            onClick={clearAll}
                            initial={{ opacity: 0 }}
                            animate={{ opacity: 1 }}
                            exit={{ opacity: 0 }}
                            transition={FADE}
                            className={cn(
                              "ml-auto h-8 whitespace-nowrap rounded-lg px-2 text-[12.5px] text-white/45 underline-offset-4 transition-colors hover:text-white hover:underline",
                              FOCUS
                            )}
                          >
                            Clear all
                          </motion.button>
                        )}
                      </AnimatePresence>
                    </motion.div>
                  </AnimatePresence>
                </div>
              </LayoutGroup>
            </motion.div>
          </AnimatePresence>
        </div>
      </motion.div>

      {/* popover */}
      <AnimatePresence>
        {pop && popPos && (
          <motion.div
            ref={popRef}
            role="dialog"
            aria-label={
              pop.step === "field"
                ? "Choose a property"
                : pop.step === "operator"
                  ? `${popField?.label ?? ""}: choose an operator`
                  : `${popField?.label ?? ""} ${popOp ?? ""}: choose a value`
            }
            initial={reduced ? { opacity: 0 } : { opacity: 0, y: -6, scale: 0.97 }}
            animate={{ opacity: 1, y: 0, scale: 1 }}
            exit={reduced ? { opacity: 0 } : { opacity: 0, y: -4, scale: 0.98 }}
            transition={reduced ? FADE : { duration: 0.18, ease: EASE }}
            style={{ left: popPos.left, top: popPos.top, width: popPos.width, transformOrigin: "top left" }}
            className="absolute z-50 overflow-hidden rounded-xl border border-white/10 bg-[#141416] shadow-[0_28px_60px_-16px_rgba(0,0,0,0.85),0_0_0_1px_rgba(0,0,0,0.45)]"
          >
            {/* step header */}
            <div className="flex h-10 items-center gap-1.5 border-b border-white/[0.06] px-1.5">
              {pop.step !== "field" ? (
                <button
                  type="button"
                  aria-label="Back"
                  onClick={() => go(pop.step === "value" ? "operator" : "field")}
                  className={cn("grid size-7 place-items-center rounded-md text-white/50 hover:bg-white/[0.06] hover:text-white", FOCUS)}
                >
                  {Icon.back}
                </button>
              ) : (
                <span className="grid size-7 place-items-center text-white/40">{Icon.funnel}</span>
              )}
              <div className="flex min-w-0 flex-1 items-center gap-1.5 text-[12.5px]">
                {pop.step === "field" ? (
                  <span className="font-medium text-white/80">{draft.editingId ? "Change property" : "Add filter"}</span>
                ) : (
                  <>
                    <span className="text-white/40">{popField?.icon ?? (popField && TYPE_ICON[popField.type])}</span>
                    <span className="truncate font-medium text-white/85">{popField?.label}</span>
                    {pop.step === "value" && popOp && (
                      <span className={cn("shrink-0 text-white/40", popOp?.length === 1 && "font-mono")}>{popOp}</span>
                    )}
                  </>
                )}
              </div>
              <StepDots step={pop.step} />
            </div>

            <PopBody stepKey={pop.step} dir={pop.dir} reduced={reduced}>
              {body}
            </PopBody>

            {pop.step === "value" && popField && popField.type !== "boolean" && (
              <div className="flex items-center gap-2 border-t border-white/[0.06] px-2 py-2">
                <span className="min-w-0 flex-1 truncate pl-1 text-[12px] text-white/40">
                  {popField.type === "select"
                    ? `${draft.sel.length} selected`
                    : popField.type === "date"
                      ? draft.preset
                        ? PRESET_LABEL[draft.preset]
                        : draft.dateA
                          ? draft.operator === "between"
                            ? `${fmtDay(draft.dateA)} – ${draft.dateB ? fmtDay(draft.dateB) : "…"}`
                            : fmtDay(draft.dateA)
                          : "Pick a date"
                      : "Enter to apply"}
                </span>
                <button
                  type="button"
                  onClick={() => apply()}
                  disabled={!canApply}
                  className={cn(
                    "h-8 rounded-lg px-3.5 text-[12.5px] font-semibold transition-[filter,opacity,transform] duration-150 hover:brightness-110 active:scale-[0.97] disabled:pointer-events-none disabled:opacity-35",
                    "outline-none focus-visible:ring-2 focus-visible:ring-white/70 focus-visible:ring-offset-2 focus-visible:ring-offset-[#141416]"
                  )}
                  style={{ background: "var(--sift-accent)", color: "var(--sift-ink)" }}
                >
                  {draft.editingId ? "Update" : "Apply"}
                </button>
              </div>
            )}
          </motion.div>
        )}
      </AnimatePresence>

      <span role="status" aria-live="polite" className="sr-only">
        {announce}
      </span>
    </div>
  );
}

/* ================================================================ internals */

function usePrev<T>(value: T): T | undefined {
  const [state, setState] = React.useState<{ current: T; previous: T | undefined }>({ current: value, previous: undefined });
  if (!Object.is(state.current, value)) setState({ current: value, previous: state.current });
  return state.previous;
}

function ChipPart({
  part,
  label,
  onClick,
  className,
  children,
}: {
  part: string;
  label: string;
  onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
  className?: string;
  children: React.ReactNode;
}) {
  return (
    <button
      type="button"
      data-part={part}
      aria-label={label}
      onClick={onClick}
      className={cn(
        "flex min-w-0 items-center px-2 text-[12.5px] transition-colors hover:bg-white/[0.05] focus-visible:bg-white/[0.07] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--sift-accent)]",
        part !== "field" && "border-l border-white/[0.06]",
        className
      )}
    >
      {children}
    </button>
  );
}

function StepDots({ step }: { step: Step }) {
  const n = STEP_INDEX[step];
  return (
    <span aria-hidden className="flex items-center gap-1 pr-1.5">
      {[0, 1, 2].map((i) => (
        <span
          key={i}
          className="h-1 rounded-full transition-all duration-300"
          style={{
            width: i === n ? 12 : 4,
            background: i <= n ? "var(--sift-accent)" : "rgba(255,255,255,0.15)",
            opacity: i === n ? 1 : i < n ? 0.5 : 1,
          }}
        />
      ))}
    </span>
  );
}

/** Slides step content horizontally and springs the popover height to fit. */
function PopBody({
  stepKey,
  dir,
  reduced,
  children,
}: {
  stepKey: string;
  dir: 1 | -1;
  reduced: boolean;
  children: React.ReactNode;
}) {
  const inner = React.useRef<HTMLDivElement>(null);
  const [h, setH] = React.useState<number | "auto">("auto");
  React.useEffect(() => {
    const el = inner.current;
    if (!el) return;
    const ro = new ResizeObserver(() => setH(el.offsetHeight));
    ro.observe(el);
    return () => ro.disconnect();
  }, []);
  return (
    <motion.div
      animate={{ height: h }}
      initial={false}
      transition={reduced ? { duration: 0 } : SLIDE}
      className="relative overflow-hidden"
    >
      <div ref={inner} className="relative">
        <AnimatePresence mode="popLayout" initial={false} custom={dir}>
          <motion.div
            key={stepKey}
            custom={dir}
            variants={{
              enter: (d: number) => (reduced ? { opacity: 0 } : { opacity: 0, x: d * 28 }),
              center: { opacity: 1, x: 0 },
              exit: (d: number) => (reduced ? { opacity: 0 } : { opacity: 0, x: d * -28 }),
            }}
            initial="enter"
            animate="center"
            exit="exit"
            transition={reduced ? FADE : { ...SLIDE, opacity: { duration: 0.16 } }}
            className="w-full"
          >
            {children}
          </motion.div>
        </AnimatePresence>
      </div>
    </motion.div>
  );
}
```
