# Sift — Animated App UI Block for React

> 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.

- Page: https://crucibleui.com/components/app/sift
- Category: [app](https://crucibleui.com/components/app)
- Tier: free (full source)
- Engine: motion
- npm dependencies: motion

## Install

```bash
npx shadcn@latest add @crucible/sift
```

Registry JSON: https://crucibleui.com/r/sift.json

Import from `@/components/crucible/sift`.

## Props (`SiftProps`)

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

Also accepts: `Omit<React.ComponentPropsWithoutRef<"div">, "onChange" | "defaultValue" | "children">`.

## Usage example

```tsx
"use client";

import { AnimatePresence, motion } from "motion/react";
import * as React from "react";

import { useReducedMotion } from "@/hooks/use-reduced-motion";
import {
  applyFilters,
  defaultSiftFields,
  defaultSiftViews,
  Sift,
  type SiftConjunction,
  type SiftFilter,
} from "@/components/crucible/sift";

const TITLES = [
  "Webhook retries ignore backoff ceiling",
  "Export to CSV drops trailing columns",
  "Onboarding checklist skips team step",
  "Rate limiter counts preflight requests",
  "Dark mode contrast on disabled inputs",
  "Search index lags behind edits",
  "Billing email shows wrong currency",
  "Invite link expires too early",
  "Audit log pagination repeats rows",
  "Slow cold start on report builder",
  "Keyboard trap in date picker",
  "SSO callback loses deep link",
  "Bulk archive times out over 500 items",
  "Sidebar flickers on route change",
  "Mobile nav overlaps toast stack",
  "Flaky test: session refresh race",
  "Attachment previews fail for HEIC",
  "Timezone drift in scheduled digests",
  "Usage chart double-counts retries",
  "Comment mentions not notifying",
  "API keys page lacks last-used date",
  "Draft autosave conflicts on two tabs",
  "Workspace rename doesn't propagate",
  "Empty state copy for new projects",
  "Migrate cron jobs to queue workers",
  "CSV import chokes on BOM header",
  "Tooltip clipped inside table cells",
  "Password reset email lands in spam",
  "Sort by priority ignores none",
  "Add retry button to failed syncs",
  "Memory leak in live cursor overlay",
  "Inline code wraps mid-token",
  "Permissions matrix missing guest role",
  "Pagination resets after filter change",
  "Activity feed shows future timestamps",
  "Reduce bundle size of editor chunk",
  "Avatar upload ignores EXIF rotation",
  "Status page link 404s in footer",
  "Integration tokens not rotated",
  "Changelog modal steals focus",
];

const STATUSES = ["Backlog", "Todo", "In progress", "In review", "Done", "Canceled"];
const PRIORITIES = ["Urgent", "High", "Medium", "Low", "None"];
const PEOPLE = ["Mara Quill", "Jonah Reyes", "Ines Varga", "Kofi Mensah", "Priya Raman", "Unassigned"];

function mulberry32(seed: number) {
  let a = seed >>> 0;
  return () => {
    a = (a + 0x6d2b79f5) >>> 0;
    let t = a;
    t = Math.imul(t ^ (t >>> 15), t | 1);
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

interface Issue {
  id: string;
  title: string;
  status: string;
  priority: string;
  assignee: string;
  estimate: number;
  daysAgo: number;
  failing: boolean;
}

const ISSUES: Issue[] = (() => {
  const r = mulberry32(1307);
  const pick = <T,>(xs: T[], w?: number[]) => {
    if (!w) return xs[Math.floor(r() * xs.length)];
    let x = r() * w.reduce((s, v) => s + v, 0);
    for (let i = 0; i < xs.length; i++) if ((x -= w[i]) < 0) return xs[i];
    return xs[xs.length - 1];
  };
  return TITLES.map((title, i) => ({
    id: `ENG-${214 + i * 3}`,
    title,
    status: pick(STATUSES, [3, 5, 6, 4, 4, 1]),
    priority: pick(PRIORITIES, [2, 4, 5, 4, 1]),
    assignee: i % 4 === 0 ? "Mara Quill" : pick(PEOPLE, [1, 3, 3, 3, 3, 1]),
    estimate: pick([1, 2, 3, 5, 8, 13], [3, 4, 4, 3, 2, 1]),
    daysAgo: Math.floor(r() ** 1.6 * 40),
    failing: r() < 0.28,
  }));
})();

const COLORS: Record<string, string> = {
  Backlog: "#71717a",
  Todo: "#d4d4d8",
  "In progress": "#fbbf24",
  "In review": "#818cf8",
  Done: "#4ade80",
  Canceled: "#52525b",
};
const PRIORITY_BARS: Record<string, number> = { Urgent: 4, High: 3, Medium: 2, Low: 1, None: 0 };
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];

const pad = (n: number) => String(n).padStart(2, "0");
const keyOf = (d: Date) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
const noop = () => () => {};

function StatusGlyph({ status }: { status: string }) {
  const c = COLORS[status] ?? "#71717a";
  const fill = status === "Done" ? 1 : status === "In review" ? 0.75 : status === "In progress" ? 0.5 : 0;
  return (
    <svg viewBox="0 0 14 14" className="size-3.5 shrink-0" aria-hidden>
      <circle cx="7" cy="7" r="5.5" fill="none" stroke={c} strokeWidth="1.5" strokeDasharray={status === "Backlog" ? "2 1.7" : undefined} />
      {fill > 0 && (
        <circle
          cx="7"
          cy="7"
          r="2.75"
          fill="none"
          stroke={c}
          strokeWidth="5.5"
          strokeDasharray={`${fill * 17.28} 17.28`}
          transform="rotate(-90 7 7)"
        />
      )}
      {status === "Canceled" && <path d="m5 5 4 4m0-4-4 4" stroke={c} strokeWidth="1.4" strokeLinecap="round" />}
    </svg>
  );
}

function PriorityGlyph({ p }: { p: string }) {
  if (p === "Urgent")
    return (
      <span aria-hidden className="grid size-3.5 place-items-center rounded-[4px] bg-[#f87171]/90 text-[9px] font-bold text-black">
        !
      </span>
    );
  const n = PRIORITY_BARS[p] ?? 0;
  return (
    <svg viewBox="0 0 14 14" className="size-3.5" aria-hidden>
      {[0, 1, 2].map((i) => (
        <rect key={i} x={2 + i * 4} y={9 - i * 3} width="2.5" height={3 + i * 3} rx="0.75" fill={i < n ? "#d4d4d8" : "rgba(255,255,255,0.15)"} />
      ))}
    </svg>
  );
}

export default function SiftDemo() {
  const reduced = useReducedMotion();
  // "Today" resolves on the client only (hydration-safe); a fixed date stands in on the server.
  const today = React.useSyncExternalStore(noop, () => keyOf(new Date()), () => "2026-10-16");
  const rows = React.useMemo(() => {
    const base = new Date(`${today}T12:00:00`);
    return ISSUES.map((it) => {
      const d = new Date(base);
      d.setDate(d.getDate() - it.daysAgo);
      return { ...it, updated: keyOf(d) };
    });
  }, [today]);

  const [filters, setFilters] = React.useState<SiftFilter[]>(defaultSiftViews[1].filters);
  const [conj, setConj] = React.useState<SiftConjunction>("and");
  const shown = React.useMemo(
    () => applyFilters(rows, filters, defaultSiftFields, { conjunction: conj, now: today }),
    [rows, filters, conj, today]
  );

  return (
    <div className="relative isolate flex h-full min-h-[620px] w-full flex-col items-center gap-3 overflow-hidden rounded-xl bg-neutral-950 px-3 py-5 sm:px-6">
      <div
        aria-hidden
        className="pointer-events-none absolute inset-0 -z-10"
        style={{
          background:
            "radial-gradient(60% 40% at 50% 0%, rgba(228,228,231,0.06), transparent 70%), radial-gradient(40% 40% at 50% 100%, rgba(255,255,255,0.025), transparent 70%)",
        }}
      />
      <p className="relative z-10 text-xs text-white/45">Add a filter — click a chip to edit it.</p>
      <div className="relative z-10 flex w-full max-w-3xl flex-1 flex-col gap-3">
        <Sift
          className="relative z-20"
          value={filters}
          onChange={setFilters}
          conjunction={conj}
          onConjunctionChange={setConj}
          count={shown.length}
          total={rows.length}
          noun={{ one: "issue", other: "issues" }}
          today={today}
          onSaveView={() => new Promise<void>((r) => window.setTimeout(r, 650))}
        />
        <div className="relative min-h-0 flex-1 overflow-hidden rounded-2xl border border-white/[0.07] bg-[#0e0e10]">
          <div className="absolute inset-0 overflow-y-auto [scrollbar-width:thin]">
            <ul aria-label="Issues" className="divide-y divide-white/[0.05]">
              <AnimatePresence initial={false}>
                {shown.map((it) => {
                  const d = new Date(`${it.updated}T12:00:00`);
                  return (
                    <motion.li
                      key={it.id}
                      layout={reduced ? false : "position"}
                      initial={{ opacity: 0 }}
                      animate={{ opacity: 1 }}
                      exit={{ opacity: 0 }}
                      transition={{ duration: reduced ? 0.12 : 0.2 }}
                      className="flex h-10 items-center gap-3 px-3.5 text-[13px]"
                    >
                      <PriorityGlyph p={it.priority} />
                      <span className="hidden w-16 shrink-0 font-mono text-[11.5px] text-white/35 sm:inline">{it.id}</span>
                      <StatusGlyph status={it.status} />
                      <span className="min-w-0 flex-1 truncate text-white/85">{it.title}</span>
                      {it.failing && (
                        <span className="hidden shrink-0 rounded-md border border-red-400/20 bg-red-400/[0.08] px-1.5 py-px text-[11px] text-red-300/90 sm:inline">
                          Failing
                        </span>
                      )}
                      <span className="hidden w-6 shrink-0 text-right text-[11.5px] tabular-nums text-white/35 sm:inline">
                        {it.estimate}
                      </span>
                      <span className="w-12 shrink-0 text-right text-[11.5px] tabular-nums text-white/35">
                        {MONTHS[d.getMonth()]} {d.getDate()}
                      </span>
                      <span
                        title={it.assignee}
                        className="grid size-6 shrink-0 place-items-center rounded-full border border-white/10 bg-white/[0.06] text-[10px] font-semibold text-white/70"
                      >
                        {it.assignee === "Unassigned"
                          ? "–"
                          : it.assignee
                              .split(" ")
                              .map((s) => s[0])
                              .join("")}
                      </span>
                    </motion.li>
                  );
                })}
              </AnimatePresence>
            </ul>
            {!shown.length && (
              <div className="grid h-full min-h-48 place-items-center px-6 text-center">
                <div>
                  <p className="text-[13px] font-medium text-white/70">No issues match</p>
                  <p className="mt-1 text-[12.5px] text-white/40">Loosen a filter or switch to “or”.</p>
                </div>
              </div>
            )}
          </div>
        </div>
      </div>
    </div>
  );
}
```

## Accessibility & performance

Honors `prefers-reduced-motion` with a designed static frame or fade (never a blank box) and passes `className` through.

## Source (`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>
  );
}
```
