{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "filter-bar",
  "type": "registry:ui",
  "title": "FilterBar",
  "description": "Filter chips that read as sentences in a one-tab-stop toolbar, with typed value editors (text, option checklists with facet counts, yes/no, numbers and ranges, date presets and calendars, date ranges, last/next N units, calendar periods, custom type editors), nested AND/OR groups with a group editor, optional per-chip impact counts and an empty-result Relax suggestion, undo/redo, narrow-width collapsing, polite result announcements and a composable parts API over the filter-core model, and server-mode facet counts through a getFacets callback (cached per field and filter, abortable, with reserved space while loading).",
  "dependencies": [
    "@internationalized/date@^3.12.4",
    "react-aria-components@^1.21.1"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://components.dethink.co.uk/r/dethink-base.json",
    "https://components.dethink.co.uk/r/filter-core.json",
    "https://components.dethink.co.uk/r/popover.json",
    "https://components.dethink.co.uk/r/input.json",
    "https://components.dethink.co.uk/r/number-input.json",
    "https://components.dethink.co.uk/r/calendar.json",
    "https://components.dethink.co.uk/r/live-region.json"
  ],
  "files": [
    {
      "path": "packages/components/src/components/filter-builder/use-filter-state.ts",
      "type": "registry:file",
      "target": "~/components/dethink/components/filter-builder/use-filter-state.ts",
      "content": "import { useCallback, useMemo, useRef, useState } from \"react\";\nimport {\n  addFilterNode,\n  clearFilter,\n  createFilter,\n  findFilterNode,\n  moveFilterNode,\n  removeFilterNode,\n  shiftFilterNode,\n  unwrapFilterGroup,\n  updateFilterCondition,\n  updateFilterGroup,\n  wrapFilterNode,\n} from \"./filter-core\";\nimport type {\n  Filter,\n  FilterCombinator,\n  FilterCondition,\n  FilterNode,\n} from \"./filter-types\";\n\nexport interface FilterCommitOptions {\n  /**\n   * Consecutive commits with the same key merge into one history entry, so a\n   * whole editing session (typing, toggling options) undoes in one step.\n   */\n  coalesceKey?: string;\n  /** Set to false to change the filter without recording history. */\n  history?: boolean;\n}\n\nexport interface UseFilterStateOptions {\n  value?: Filter;\n  defaultValue?: Filter;\n  onValueChange?: (filter: Filter) => void;\n  /** Maximum undo steps kept. Defaults to 50. */\n  historyLimit?: number;\n}\n\nexport interface FilterState {\n  filter: Filter;\n  setFilter: (\n    next: Filter | ((current: Filter) => Filter),\n    options?: FilterCommitOptions,\n  ) => void;\n  addNode: (\n    node: FilterNode,\n    options?: FilterCommitOptions & { index?: number; parentId?: string },\n  ) => void;\n  updateCondition: (\n    id: string,\n    patch: Partial<Omit<FilterCondition, \"id\" | \"type\">>,\n    options?: FilterCommitOptions,\n  ) => void;\n  removeNode: (id: string, options?: FilterCommitOptions) => void;\n  setCombinator: (\n    groupId: string,\n    combinator: FilterCombinator,\n    options?: FilterCommitOptions,\n  ) => void;\n  /** Negates or un-negates a condition or group. */\n  setNegated: (\n    id: string,\n    negated: boolean,\n    options?: FilterCommitOptions,\n  ) => void;\n  /** Wraps a node in a new group that takes its place. */\n  wrapInGroup: (\n    id: string,\n    options?: FilterCommitOptions & {\n      combinator?: FilterCombinator;\n      groupId?: string;\n    },\n  ) => void;\n  /** Replaces a group with its children. */\n  unwrapGroup: (id: string, options?: FilterCommitOptions) => void;\n  moveNode: (\n    id: string,\n    target: { index?: number; parentId: string },\n    options?: FilterCommitOptions,\n  ) => void;\n  /** Moves a node one place up (-1) or down (1) among its siblings. */\n  shiftNode: (\n    id: string,\n    offset: -1 | 1,\n    options?: FilterCommitOptions,\n  ) => void;\n  clear: (options?: FilterCommitOptions) => void;\n  undo: () => void;\n  redo: () => void;\n  canUndo: boolean;\n  canRedo: boolean;\n}\n\ninterface FilterHistory {\n  past: Filter[];\n  future: Filter[];\n  coalesceKey?: string;\n}\n\nconst emptyHistory: FilterHistory = { past: [], future: [] };\n\n/**\n * Controlled or uncontrolled filter state with undo/redo. Every command is a\n * single history entry.\n */\nexport function useFilterState({\n  defaultValue,\n  historyLimit = 50,\n  onValueChange,\n  value,\n}: UseFilterStateOptions = {}): FilterState {\n  const [internalFilter, setInternalFilter] = useState<Filter>(\n    () => defaultValue ?? createFilter({ id: \"root\" }),\n  );\n  const [history, setHistoryState] = useState<FilterHistory>(emptyHistory);\n  const isControlled = value !== undefined;\n  const filter = value ?? internalFilter;\n  const filterRef = useRef(filter);\n  const historyRef = useRef(history);\n  const onValueChangeRef = useRef(onValueChange);\n\n  filterRef.current = filter;\n  historyRef.current = history;\n  onValueChangeRef.current = onValueChange;\n\n  const setHistory = useCallback((next: FilterHistory) => {\n    historyRef.current = next;\n    setHistoryState(next);\n  }, []);\n\n  const apply = useCallback(\n    (next: Filter) => {\n      filterRef.current = next;\n\n      if (!isControlled) {\n        setInternalFilter(next);\n      }\n\n      onValueChangeRef.current?.(next);\n    },\n    [isControlled],\n  );\n\n  const setFilter = useCallback<FilterState[\"setFilter\"]>(\n    (nextOrUpdater, { coalesceKey, history: record = true } = {}) => {\n      const current = filterRef.current;\n      const next =\n        typeof nextOrUpdater === \"function\"\n          ? nextOrUpdater(current)\n          : nextOrUpdater;\n\n      if (next === current) {\n        return;\n      }\n\n      if (record) {\n        const previous = historyRef.current;\n        const coalesce =\n          coalesceKey !== undefined &&\n          previous.coalesceKey === coalesceKey &&\n          previous.past.length > 0;\n\n        setHistory({\n          past: coalesce\n            ? previous.past\n            : [...previous.past, current].slice(-historyLimit),\n          future: [],\n          coalesceKey,\n        });\n      }\n\n      apply(next);\n    },\n    [apply, historyLimit, setHistory],\n  );\n\n  const undo = useCallback(() => {\n    const { future, past } = historyRef.current;\n    const previous = past.at(-1);\n\n    if (!previous) {\n      return;\n    }\n\n    setHistory({\n      past: past.slice(0, -1),\n      future: [filterRef.current, ...future],\n    });\n    apply(previous);\n  }, [apply, setHistory]);\n\n  const redo = useCallback(() => {\n    const { future, past } = historyRef.current;\n    const [next, ...rest] = future;\n\n    if (!next) {\n      return;\n    }\n\n    setHistory({ past: [...past, filterRef.current], future: rest });\n    apply(next);\n  }, [apply, setHistory]);\n\n  return useMemo<FilterState>(\n    () => ({\n      filter,\n      setFilter,\n      addNode: (node, { index, parentId, ...options } = {}) => {\n        setFilter(\n          (current) => addFilterNode(current, node, { index, parentId }),\n          options,\n        );\n      },\n      updateCondition: (id, patch, options) => {\n        setFilter(\n          (current) => updateFilterCondition(current, id, patch),\n          options,\n        );\n      },\n      removeNode: (id, options) => {\n        setFilter((current) => removeFilterNode(current, id), options);\n      },\n      setCombinator: (groupId, combinator, options) => {\n        setFilter(\n          (current) => updateFilterGroup(current, groupId, { combinator }),\n          options,\n        );\n      },\n      setNegated: (id, negated, options) => {\n        setFilter((current) => {\n          const patch = { not: negated || undefined };\n\n          return current.id === id ||\n            findFilterNode(current, id)?.type === \"group\"\n            ? updateFilterGroup(current, id, patch)\n            : updateFilterCondition(current, id, patch);\n        }, options);\n      },\n      wrapInGroup: (id, { combinator, groupId, ...options } = {}) => {\n        setFilter(\n          (current) => wrapFilterNode(current, id, { combinator, groupId }),\n          options,\n        );\n      },\n      unwrapGroup: (id, options) => {\n        setFilter((current) => unwrapFilterGroup(current, id), options);\n      },\n      moveNode: (id, target, options) => {\n        setFilter((current) => moveFilterNode(current, id, target), options);\n      },\n      shiftNode: (id, offset, options) => {\n        setFilter((current) => shiftFilterNode(current, id, offset), options);\n      },\n      clear: (options) => {\n        setFilter(\n          (current) =>\n            current.children.length === 0 ? current : clearFilter(current),\n          options,\n        );\n      },\n      undo,\n      redo,\n      canUndo: history.past.length > 0,\n      canRedo: history.future.length > 0,\n    }),\n    [filter, history, redo, setFilter, undo],\n  );\n}\n"
    },
    {
      "path": "packages/components/src/components/filter-builder/use-filter-insights.ts",
      "type": "registry:file",
      "target": "~/components/dethink/components/filter-builder/use-filter-insights.ts",
      "content": "import { useMemo } from \"react\";\nimport {\n  computeFilterFacets,\n  computeFilterImpact,\n  countFilterMatches,\n  findFilterRescue,\n  type FilterFacetTarget,\n  type FilterRescue,\n} from \"./filter-core\";\nimport type {\n  FilterEvaluateOptions,\n  FilterFields,\n  FilterNode,\n} from \"./filter-types\";\n\nexport interface UseFilterInsightsOptions<TData> {\n  /** Rows to count against. Without rows, every insight is undefined. */\n  data?: readonly TData[];\n  filter: FilterNode;\n  fields: FilterFields<TData>;\n  evaluateOptions?: FilterEvaluateOptions;\n  /** Compute per-condition impact counts. */\n  impact?: boolean;\n  /** Suggest the most restrictive condition when nothing matches. */\n  rescue?: boolean;\n}\n\nexport interface FilterInsights {\n  /** Matching rows, when rows were given. */\n  count?: number;\n  /**\n   * Rows per value of a field under the rest of the filter. Pass the target\n   * condition (or the group a new one joins) for counts that match the pick;\n   * undefined where counts would be wrong. See `computeFilterFacets`.\n   */\n  getFacets?: (\n    fieldKey: string,\n    target?: FilterFacetTarget,\n  ) => ReadonlyMap<string, number> | undefined;\n  /**\n   * Rows removed by each active condition, keyed by condition id. Negative\n   * when the condition adds rows (an OR branch).\n   */\n  impact?: ReadonlyMap<string, number>;\n  rescue?: FilterRescue;\n}\n\n/**\n * Client-side counts for a filter over `data`: result count, facet counts\n * (computed lazily per field and cached until the filter or rows change),\n * per-condition impact, and an empty-result rescue. Costs grow with rows ×\n * conditions; for large or server-side data, pass counts from your API.\n */\nexport function useFilterInsights<TData>({\n  data,\n  evaluateOptions,\n  fields,\n  filter,\n  impact: withImpact = false,\n  rescue: withRescue = true,\n}: UseFilterInsightsOptions<TData>): FilterInsights {\n  const count = useMemo(\n    () =>\n      data\n        ? countFilterMatches(data, filter, fields, evaluateOptions)\n        : undefined,\n    [data, evaluateOptions, fields, filter],\n  );\n\n  const getFacets = useMemo(() => {\n    if (!data) {\n      return undefined;\n    }\n\n    const cache = new Map<string, ReadonlyMap<string, number> | undefined>();\n\n    return (fieldKey: string, target?: FilterFacetTarget) => {\n      const key = JSON.stringify([\n        fieldKey,\n        target?.conditionId,\n        target?.parentId,\n        target?.operator,\n      ]);\n      let facets = cache.get(key);\n\n      if (!cache.has(key)) {\n        facets = computeFilterFacets(data, filter, fields, fieldKey, {\n          ...evaluateOptions,\n          target,\n        });\n        cache.set(key, facets);\n      }\n\n      return facets;\n    };\n  }, [data, evaluateOptions, fields, filter]);\n\n  const impact = useMemo(\n    () =>\n      data && withImpact\n        ? computeFilterImpact(data, filter, fields, evaluateOptions)\n        : undefined,\n    [data, evaluateOptions, fields, filter, withImpact],\n  );\n\n  const rescue = useMemo(\n    () =>\n      data && withRescue && count === 0\n        ? findFilterRescue(data, filter, fields, evaluateOptions)\n        : undefined,\n    [count, data, evaluateOptions, fields, filter, withRescue],\n  );\n\n  return { count, getFacets, impact, rescue };\n}\n"
    },
    {
      "path": "packages/components/src/components/filter-builder/use-server-facets.ts",
      "type": "registry:file",
      "target": "~/components/dethink/components/filter-builder/use-server-facets.ts",
      "content": "import {\n  useCallback,\n  useEffect,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport {\n  getFilterFacetFilter,\n  getFilterSignature,\n  type FilterFacetTarget,\n} from \"./filter-core\";\nimport type { Filter, FilterFields } from \"./filter-types\";\n\n/** What a server facet request asks for. */\nexport interface FilterFacetRequest {\n  /** Field whose option counts are wanted. */\n  field: string;\n  /**\n   * The filter to count under: the current filter without the condition the\n   * counts are for, only active conditions, normalized.\n   */\n  filter: Filter;\n  signal: AbortSignal;\n}\n\n/** Counts per option value, in any of these shapes. */\nexport type FilterFacetCounts =\n  | ReadonlyMap<string, number>\n  | Readonly<Record<string, number>>\n  | readonly { value: string; count: number }[];\n\nexport type GetFilterFacets = (\n  request: FilterFacetRequest,\n) => Promise<FilterFacetCounts>;\n\nexport type FilterFacetStatus = \"loading\" | \"ready\" | \"error\" | \"unsupported\";\n\nexport interface ServerFacets {\n  getFacets: (\n    fieldKey: string,\n    target?: FilterFacetTarget,\n  ) => ReadonlyMap<string, number> | undefined;\n  getFacetStatus: (\n    fieldKey: string,\n    target?: FilterFacetTarget,\n  ) => FilterFacetStatus;\n}\n\ninterface Entry {\n  status: \"loading\" | \"ready\" | \"error\";\n  counts?: ReadonlyMap<string, number>;\n  /** When an error happened, for retries. */\n  at?: number;\n}\n\nconst MAX_ENTRIES = 60;\n\n// Layout effects mark the bar mounted before a render's microtask runs;\n// on the server (no effects run) plain useEffect avoids React 18's warning.\nconst useIsomorphicLayoutEffect =\n  typeof window === \"undefined\" ? useEffect : useLayoutEffect;\n/** A failed request is asked again after this long, on the next render. */\nconst RETRY_AFTER = 10_000;\n\nfunction toMap(counts: FilterFacetCounts): ReadonlyMap<string, number> {\n  if (counts instanceof Map) {\n    return counts;\n  }\n\n  if (Array.isArray(counts)) {\n    return new Map(\n      (counts as readonly { value: string; count: number }[]).map((item) => [\n        String(item.value),\n        Number(item.count) || 0,\n      ]),\n    );\n  }\n\n  return new Map(\n    Object.entries(counts as Record<string, number>).map(([key, count]) => [\n      key,\n      Number(count) || 0,\n    ]),\n  );\n}\n\nfunction stringifyKey(value: unknown) {\n  try {\n    return JSON.stringify(value) ?? \"\";\n  } catch {\n    return String(value);\n  }\n}\n\n/**\n * Facet counts from your server. Pickers ask for a field's counts while\n * they render; each (facetsKey, field, filter) is requested once, cached,\n * and aborted when no longer needed. `getFacets` is read through a ref, so\n * an inline function is fine; change `facetsKey` (e.g. a time range or a\n * data version) when the same filter should count differently. `facetsKey`\n * is compared as JSON, so use a primitive or plain JSON value (not a Map,\n * Set, class instance or function). Until counts\n * arrive the status is \"loading\", so pickers can reserve their space.\n */\nexport function useServerFacets<TData>({\n  facetsKey,\n  fields,\n  filter,\n  getFacets: fetchFacets,\n}: {\n  fields: FilterFields<TData>;\n  filter: Filter;\n  getFacets?: GetFilterFacets;\n  facetsKey?: unknown;\n}): ServerFacets | undefined {\n  const [entries, setEntries] = useState<ReadonlyMap<string, Entry>>(\n    () => new Map(),\n  );\n  const wantedRef = useRef(\n    new Map<\n      string,\n      { field: string; filter: Filter; fetcher: GetFilterFacets }\n    >(),\n  );\n  const inFlightRef = useRef(new Map<string, AbortController>());\n  const fetchRef = useRef(fetchFacets);\n  const entriesRef = useRef(entries);\n  const scheduledRef = useRef(false);\n  const mountedRef = useRef(false);\n  const source = stringifyKey(facetsKey);\n  const sourceRef = useRef(source);\n\n  fetchRef.current = fetchFacets;\n  entriesRef.current = entries;\n  sourceRef.current = source;\n\n  // Aborts in-flight requests for a facetsKey that is no longer current:\n  // their entries are then forgotten, so a later render asks again.\n  useEffect(() => {\n    const inFlight = inFlightRef.current;\n\n    // Only this key's requests: ones for the next key may already run.\n    const prefix = `${source}\\u0000`;\n\n    return () => {\n      for (const [key, controller] of inFlight) {\n        if (key.startsWith(prefix)) {\n          controller.abort();\n          inFlight.delete(key);\n        }\n      }\n    };\n  }, [source]);\n\n  // Starts requests that renders asked for. Pickers render in their own\n  // commits (popovers), so this runs from a microtask, not an effect here.\n  // Only while mounted (requests asked for before the commit wait for it),\n  // and only for the current facetsKey.\n  const flush = useCallback(() => {\n    scheduledRef.current = false;\n\n    if (!mountedRef.current) {\n      return;\n    }\n\n    const wanted = [...wantedRef.current];\n    const prefix = `${sourceRef.current}\\u0000`;\n\n    wantedRef.current.clear();\n\n    for (const [key, request] of wanted) {\n      if (!key.startsWith(prefix)) {\n        continue;\n      }\n\n      const existing = entriesRef.current.get(key);\n\n      if (\n        inFlightRef.current.has(key) ||\n        existing?.status === \"ready\" ||\n        (existing?.status === \"error\" &&\n          Date.now() - (existing.at ?? 0) < RETRY_AFTER)\n      ) {\n        continue;\n      }\n\n      const controller = new AbortController();\n\n      inFlightRef.current.set(key, controller);\n      setEntries((current) => new Map(current).set(key, { status: \"loading\" }));\n\n      const settle = (entry: Entry) => {\n        if (inFlightRef.current.get(key) === controller) {\n          inFlightRef.current.delete(key);\n        }\n\n        setEntries((current) => {\n          const next = new Map(current);\n\n          if (controller.signal.aborted) {\n            // Forget the pending entry so a later render asks again.\n            if (next.get(key)?.status === \"loading\") {\n              next.delete(key);\n            }\n\n            return next;\n          }\n\n          next.delete(key);\n          next.set(key, entry);\n\n          // Keep the cache small: drop the oldest entries.\n          while (next.size > MAX_ENTRIES) {\n            const oldest = next.keys().next().value;\n\n            if (oldest === undefined) {\n              break;\n            }\n\n            next.delete(oldest);\n          }\n\n          return next;\n        });\n      };\n\n      Promise.resolve()\n        .then(() =>\n          request.fetcher({\n            field: request.field,\n            filter: request.filter,\n            signal: controller.signal,\n          }),\n        )\n        .then(\n          (counts) => {\n            settle({ status: \"ready\", counts: toMap(counts) });\n          },\n          () => {\n            settle({ status: \"error\", at: Date.now() });\n          },\n        );\n    }\n  }, []);\n\n  const schedule = useCallback(() => {\n    if (!scheduledRef.current) {\n      scheduledRef.current = true;\n      queueMicrotask(flush);\n    }\n  }, [flush]);\n\n  // Mounted (or shown again after Activity hid it): start what renders\n  // asked for meanwhile. Unmounted (or hidden): abort everything.\n  useIsomorphicLayoutEffect(() => {\n    const inFlight = inFlightRef.current;\n\n    mountedRef.current = true;\n\n    if (wantedRef.current.size > 0) {\n      schedule();\n    }\n\n    return () => {\n      mountedRef.current = false;\n\n      for (const controller of inFlight.values()) {\n        controller.abort();\n      }\n\n      inFlight.clear();\n    };\n  }, [schedule]);\n\n  const enabled = fetchFacets !== undefined;\n\n  return useMemo(() => {\n    if (!enabled) {\n      return undefined;\n    }\n\n    const lookup = (fieldKey: string, target?: FilterFacetTarget) => {\n      const facetFilter = getFilterFacetFilter(\n        filter,\n        fields,\n        fieldKey,\n        target,\n      );\n\n      if (!facetFilter) {\n        return { supported: false, entry: undefined };\n      }\n\n      const key = `${source}\\u0000${fieldKey}\\u0000${getFilterSignature(facetFilter)}`;\n      const entry = entries.get(key);\n      const retry =\n        entry?.status === \"error\" &&\n        Date.now() - (entry.at ?? 0) >= RETRY_AFTER;\n\n      // Asked for during render; a microtask starts the request, with the\n      // getFacets of this render. Never on the server.\n      const fetcher = fetchRef.current;\n\n      if ((!entry || retry) && fetcher && typeof window !== \"undefined\") {\n        wantedRef.current.set(key, {\n          field: fieldKey,\n          filter: facetFilter,\n          fetcher,\n        });\n        schedule();\n      }\n\n      return { supported: true, entry };\n    };\n\n    return {\n      getFacets: (fieldKey, target) => lookup(fieldKey, target).entry?.counts,\n      getFacetStatus: (fieldKey, target) => {\n        const { entry, supported } = lookup(fieldKey, target);\n\n        return supported ? (entry?.status ?? \"loading\") : \"unsupported\";\n      },\n    };\n  }, [enabled, entries, fields, filter, schedule, source]);\n}\n"
    },
    {
      "path": "packages/components/src/components/filter-builder/filter-editors.tsx",
      "type": "registry:file",
      "target": "~/components/dethink/components/filter-builder/filter-editors.tsx",
      "content": "import {\n  Autocomplete,\n  Input as AriaInput,\n  ListBox,\n  ListBoxItem,\n  SearchField,\n  useFilter,\n} from \"react-aria-components\";\nimport { useEffect, type KeyboardEvent, type ReactNode } from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { inputClassNames } from \"../input\";\nimport { getFilterOperatorLabel, getFilterOperators } from \"./filter-core\";\nimport type {\n  FilterField,\n  FilterOperatorDefinition,\n  FilterValue,\n} from \"./filter-types\";\n\nexport const filterListBoxClasses =\n  \"max-h-64 min-w-0 overflow-auto outline-none\";\n\nexport const filterListItemClasses =\n  \"grid cursor-default grid-cols-[1rem_minmax(0,1fr)] items-center gap-[var(--dt-space-2)] rounded-sm px-[var(--dt-space-2)] py-[var(--dt-space-1-5)] text-sm text-foreground outline-none motion-safe:transition-[background-color,color] motion-safe:duration-[var(--dt-motion-fast)] motion-safe:ease-control data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[focus-visible]:ring-2 data-[focus-visible]:ring-inset data-[focus-visible]:ring-ring data-[focused]:bg-muted data-[hovered]:bg-muted data-[pressed]:bg-muted/80\";\n\nexport const filterListIndicatorClasses =\n  \"flex size-4 items-center justify-center text-foreground\";\n\nexport const filterListCheckboxClasses =\n  \"flex size-4 items-center justify-center rounded-[4px] border border-input bg-background text-primary-foreground group-data-[selected]:border-primary group-data-[selected]:bg-primary [&>svg]:invisible group-data-[selected]:[&>svg]:visible\";\n\nexport const filterListEmptyClasses =\n  \"px-[var(--dt-space-2)] py-[var(--dt-space-3)] text-sm text-muted-foreground\";\n\nexport const filterEditorStackClasses = \"grid min-w-0 gap-[var(--dt-space-2)]\";\n\n/**\n * Autocomplete spends the first Escape clearing its virtual focus. Close the\n * editor straight away instead when the search box is empty.\n */\nexport function closeOnEmptyEscape(onClose: (() => void) | undefined) {\n  return (event: KeyboardEvent<HTMLDivElement>) => {\n    if (\n      onClose &&\n      event.key === \"Escape\" &&\n      event.target instanceof HTMLInputElement &&\n      event.target.value === \"\"\n    ) {\n      event.preventDefault();\n      event.stopPropagation();\n      onClose();\n    }\n  };\n}\n\nexport function filterListItemClassNames({\n  className,\n}: { className?: string } = {}) {\n  return cn(filterListItemClasses, className);\n}\n\nexport function CheckIcon() {\n  return (\n    <svg aria-hidden=\"true\" viewBox=\"0 0 16 16\" className=\"size-3.5\">\n      <path\n        d=\"m3.5 8.5 3 3 6-7\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        strokeWidth=\"2\"\n      />\n    </svg>\n  );\n}\n\nexport function SearchInput({ label }: { label: string }) {\n  return (\n    // Editors open on demand, so moving focus into the search is expected.\n    // eslint-disable-next-line jsx-a11y/no-autofocus\n    <SearchField aria-label={label} autoFocus className=\"min-w-0\">\n      <AriaInput\n        placeholder={label}\n        className={inputClassNames({\n          controlSize: \"sm\",\n          className: \"[&::-webkit-search-cancel-button]:hidden\",\n        })}\n      />\n    </SearchField>\n  );\n}\n\nexport interface FilterFieldPickerProps<TData = unknown> {\n  fields: readonly FilterField<TData>[];\n  onSelect: (fieldKey: string) => void;\n  selectedKey?: string;\n  searchLabel?: string;\n  listLabel?: string;\n  emptyLabel?: ReactNode;\n  /** Closes the surrounding editor, e.g. on Escape. */\n  onClose?: () => void;\n}\n\n/** Searchable list of fields. Selecting one calls `onSelect`. */\nexport function FilterFieldPicker<TData>({\n  emptyLabel = \"No results.\",\n  fields,\n  listLabel = \"Fields\",\n  onClose,\n  onSelect,\n  searchLabel = \"Search fields\",\n  selectedKey,\n}: FilterFieldPickerProps<TData>) {\n  const { contains } = useFilter({ sensitivity: \"base\" });\n  const items = fields.map((field) => ({ id: field.key, label: field.label }));\n\n  return (\n    <div\n      data-slot=\"filter-field-picker\"\n      className={filterEditorStackClasses}\n      onKeyDownCapture={closeOnEmptyEscape(onClose)}\n    >\n      <Autocomplete filter={contains}>\n        <SearchInput label={searchLabel} />\n        <ListBox\n          aria-label={listLabel}\n          items={items}\n          className={filterListBoxClasses}\n          renderEmptyState={() => (\n            <div className={filterListEmptyClasses}>{emptyLabel}</div>\n          )}\n          onAction={(key) => {\n            onSelect(String(key));\n          }}\n        >\n          {(item) => (\n            <ListBoxItem\n              id={item.id}\n              textValue={item.label}\n              className={filterListItemClassNames()}\n            >\n              <span aria-hidden=\"true\" className={filterListIndicatorClasses}>\n                {item.id === selectedKey ? <CheckIcon /> : null}\n              </span>\n              <span className=\"min-w-0 truncate\">{item.label}</span>\n            </ListBoxItem>\n          )}\n        </ListBox>\n      </Autocomplete>\n    </div>\n  );\n}\n\nexport interface FilterOperatorPickerProps {\n  field: Pick<FilterField, \"operators\" | \"type\">;\n  onSelect: (operatorId: string) => void;\n  selectedKey?: string;\n  value?: FilterValue;\n  listLabel?: string;\n  getOperatorLabel?: (\n    operator: FilterOperatorDefinition,\n    value?: FilterValue,\n  ) => string;\n}\n\n/** List of the operators available for a field's type. */\nexport function FilterOperatorPicker({\n  field,\n  getOperatorLabel = getFilterOperatorLabel,\n  listLabel = \"Operators\",\n  onSelect,\n  selectedKey,\n  value,\n}: FilterOperatorPickerProps) {\n  const items = getFilterOperators(field).map((operator) => ({\n    id: operator.id,\n    label: getOperatorLabel(operator, value),\n  }));\n\n  return (\n    <ListBox\n      aria-label={listLabel}\n      // eslint-disable-next-line jsx-a11y/no-autofocus -- Opened on demand.\n      autoFocus=\"first\"\n      data-slot=\"filter-operator-picker\"\n      items={items}\n      className={filterListBoxClasses}\n      onAction={(key) => {\n        onSelect(String(key));\n      }}\n    >\n      {(item) => (\n        <ListBoxItem\n          id={item.id}\n          textValue={item.label}\n          className={filterListItemClassNames()}\n        >\n          <span aria-hidden=\"true\" className={filterListIndicatorClasses}>\n            {item.id === selectedKey ? <CheckIcon /> : null}\n          </span>\n          <span className=\"min-w-0 truncate\">{item.label}</span>\n        </ListBoxItem>\n      )}\n    </ListBox>\n  );\n}\n\n/**\n * Focuses an element after the popover has registered as the top overlay. A\n * native autoFocus fires first and, inside a group editor's popover, reads as\n * an outside interaction that dismisses both popovers.\n */\nexport function useDeferredFocus(\n  ref: { current: HTMLElement | null },\n  enabled = true,\n) {\n  useEffect(() => {\n    if (!enabled) {\n      return undefined;\n    }\n\n    const timer = window.setTimeout(() => {\n      ref.current?.focus();\n    }, 0);\n\n    return () => {\n      window.clearTimeout(timer);\n    };\n  }, [enabled, ref]);\n}\n"
    },
    {
      "path": "packages/components/src/components/filter-builder/filter-typed-editors.tsx",
      "type": "registry:file",
      "target": "~/components/dethink/components/filter-builder/filter-typed-editors.tsx",
      "content": "import { parseDate, type CalendarDate } from \"@internationalized/date\";\nimport { ListBox, ListBoxItem } from \"react-aria-components\";\nimport { useRef, useState } from \"react\";\nimport {\n  Calendar,\n  RangeCalendar,\n  type CalendarWeekStartsOn,\n} from \"../calendar\";\nimport { NumberInput } from \"../number-input\";\nimport {\n  formatFilterDate,\n  formatFilterDuration,\n  isFilterDate,\n  isFilterDuration,\n} from \"./filter-dates\";\nimport {\n  CheckIcon,\n  filterEditorStackClasses,\n  filterListBoxClasses,\n  filterListIndicatorClasses,\n  filterListItemClassNames,\n  useDeferredFocus,\n} from \"./filter-editors\";\nimport type {\n  FilterDate,\n  FilterDateUnit,\n  FilterDuration,\n  FilterValue,\n  FilterWeekday,\n} from \"./filter-types\";\n\nexport interface FilterTypedEditorLabels {\n  numberValue: (fieldLabel: string) => string;\n  rangeFrom: string;\n  rangeTo: string;\n  presets: string;\n  pickDate: string;\n  pickRange: string;\n  amount: string;\n  unit: string;\n  units: Record<FilterDateUnit, string>;\n}\n\nexport const defaultFilterTypedEditorLabels: FilterTypedEditorLabels = {\n  numberValue: (fieldLabel) => `${fieldLabel} value`,\n  rangeFrom: \"From\",\n  rangeTo: \"To\",\n  presets: \"Presets\",\n  pickDate: \"Pick a date\",\n  pickRange: \"Pick a date range\",\n  amount: \"Amount\",\n  unit: \"Unit\",\n  units: { day: \"days\", week: \"weeks\", month: \"months\", year: \"years\" },\n};\n\nexport interface TypedEditorProps {\n  fieldLabel: string;\n  value: FilterValue | undefined;\n  onValueChange: (value: FilterValue | undefined) => void;\n  onCommit: () => void;\n  labels: FilterTypedEditorLabels;\n  locale: string;\n  weekStartsOn: FilterWeekday;\n}\n\nconst weekdays: CalendarWeekStartsOn[] = [\n  \"sun\",\n  \"mon\",\n  \"tue\",\n  \"wed\",\n  \"thu\",\n  \"fri\",\n  \"sat\",\n];\n\nconst rowClasses = \"flex min-w-0 items-center gap-[var(--dt-space-2)]\";\n\nconst selectClasses =\n  \"h-8 rounded-md border border-input bg-background px-[var(--dt-space-2)] text-sm text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring\";\n\nfunction capitalize(text: string) {\n  return text.charAt(0).toLocaleUpperCase() + text.slice(1);\n}\n\nfunction parseNumber(text: string) {\n  if (text.trim() === \"\") {\n    return undefined;\n  }\n\n  const value = Number(text);\n\n  return Number.isFinite(value) ? value : undefined;\n}\n\nfunction sameValue(a: unknown, b: unknown) {\n  return JSON.stringify(a) === JSON.stringify(b);\n}\n\n/**\n * Local input state that follows the controlled value. The draft keeps\n * incomplete typing (\"1.\", \"-\", one end of a range) while it still stands for\n * the current value, and resets when the value changes to something else,\n * such as undo, a preset or a parent update.\n */\nfunction useValueDraft<TDraft>(\n  value: FilterValue | undefined,\n  toDraft: (value: FilterValue | undefined) => TDraft,\n  fromDraft: (draft: TDraft) => FilterValue | undefined,\n) {\n  const [state, setState] = useState(() => ({ value, draft: toDraft(value) }));\n  let { draft } = state;\n\n  if (!Object.is(state.value, value)) {\n    if (!sameValue(fromDraft(draft), value)) {\n      draft = toDraft(value);\n    }\n\n    setState({ value, draft });\n  }\n\n  const setDraft = (next: TDraft) => {\n    setState((current) => ({ ...current, draft: next }));\n  };\n\n  return [draft, setDraft] as const;\n}\n\nfunction numberText(value: unknown) {\n  return typeof value === \"number\" ? String(value) : \"\";\n}\n\nfunction toRangeDrafts(value: FilterValue | undefined): [string, string] {\n  const range = Array.isArray(value) ? value : [];\n\n  return [numberText(range[0]), numberText(range[1])];\n}\n\nfunction fromRangeDrafts([fromText, toText]: [string, string]):\n  [number, number] | undefined {\n  const from = parseNumber(fromText);\n  const to = parseNumber(toText);\n\n  // A range needs both ends; until then the chip stays incomplete.\n  return from !== undefined && to !== undefined ? [from, to] : undefined;\n}\n\ninterface DurationDraft {\n  amount: string;\n  unit: FilterDateUnit;\n}\n\nfunction toDurationDraft(value: FilterValue | undefined): DurationDraft {\n  const duration = isFilterDuration(value) ? value : undefined;\n\n  return {\n    amount: duration ? String(duration.amount) : \"\",\n    unit: duration?.unit ?? \"day\",\n  };\n}\n\nfunction fromDurationDraft({ amount, unit }: DurationDraft) {\n  const parsed = parseNumber(amount);\n\n  return parsed !== undefined && Number.isInteger(parsed) && parsed >= 1\n    ? { amount: parsed, unit }\n    : undefined;\n}\n\nfunction commitOnEnter(onCommit: () => void) {\n  return (event: { key: string; preventDefault: () => void }) => {\n    if (event.key === \"Enter\") {\n      event.preventDefault();\n      onCommit();\n    }\n  };\n}\n\ninterface Choice {\n  id: string;\n  label: string;\n  value: FilterValue;\n}\n\n/** Single-choice list that commits on pick. */\nfunction ChoiceList({\n  choices,\n  columns = false,\n  label,\n  onPick,\n  value,\n}: {\n  choices: Choice[];\n  /** Two columns, to keep editors with a calendar short. */\n  columns?: boolean;\n  label: string;\n  onPick: (value: FilterValue) => void;\n  value: FilterValue | undefined;\n}) {\n  return (\n    <ListBox\n      aria-label={label}\n      // eslint-disable-next-line jsx-a11y/no-autofocus -- Opened on demand.\n      autoFocus=\"first\"\n      items={choices}\n      className={\n        columns\n          ? `${filterListBoxClasses} grid grid-cols-2 gap-x-[var(--dt-space-1)]`\n          : filterListBoxClasses\n      }\n      onAction={(key) => {\n        const choice = choices.find((item) => item.id === key);\n\n        if (choice) {\n          onPick(choice.value);\n        }\n      }}\n    >\n      {(choice) => (\n        <ListBoxItem\n          id={choice.id}\n          textValue={choice.label}\n          className={filterListItemClassNames()}\n        >\n          <span aria-hidden=\"true\" className={filterListIndicatorClasses}>\n            {sameValue(choice.value, value) ? <CheckIcon /> : null}\n          </span>\n          <span className=\"min-w-0 truncate\">{choice.label}</span>\n        </ListBoxItem>\n      )}\n    </ListBox>\n  );\n}\n\nexport function FilterNumberEditor({\n  fieldLabel,\n  labels,\n  onCommit,\n  onValueChange,\n  value,\n}: TypedEditorProps) {\n  const inputRef = useRef<HTMLInputElement>(null);\n  const [draft, setDraft] = useValueDraft(value, numberText, parseNumber);\n\n  useDeferredFocus(inputRef);\n\n  return (\n    <div data-slot=\"filter-value-editor\" className={filterEditorStackClasses}>\n      <NumberInput\n        ref={inputRef}\n        aria-label={labels.numberValue(fieldLabel)}\n        controlSize=\"sm\"\n        value={draft}\n        onChange={(event) => {\n          const text = event.currentTarget.value;\n\n          setDraft(text);\n          onValueChange(parseNumber(text));\n        }}\n        onKeyDown={commitOnEnter(onCommit)}\n      />\n    </div>\n  );\n}\n\nexport function FilterNumberRangeEditor({\n  fieldLabel,\n  labels,\n  onCommit,\n  onValueChange,\n  value,\n}: TypedEditorProps) {\n  const inputRef = useRef<HTMLInputElement>(null);\n  const [drafts, setDrafts] = useValueDraft(\n    value,\n    toRangeDrafts,\n    fromRangeDrafts,\n  );\n\n  useDeferredFocus(inputRef);\n\n  const update = (index: 0 | 1, text: string) => {\n    const next: [string, string] = [...drafts];\n\n    next[index] = text;\n    setDrafts(next);\n    onValueChange(fromRangeDrafts(next));\n  };\n\n  return (\n    <div data-slot=\"filter-value-editor\" className={rowClasses}>\n      {([0, 1] as const).map((index) => (\n        <NumberInput\n          key={index}\n          ref={index === 0 ? inputRef : undefined}\n          aria-label={`${labels.numberValue(fieldLabel)}, ${index === 0 ? labels.rangeFrom : labels.rangeTo}`}\n          placeholder={index === 0 ? labels.rangeFrom : labels.rangeTo}\n          controlSize=\"sm\"\n          value={drafts[index]}\n          onChange={(event) => {\n            update(index, event.currentTarget.value);\n          }}\n          onKeyDown={commitOnEnter(onCommit)}\n        />\n      ))}\n    </div>\n  );\n}\n\nfunction toCalendarDate(date: FilterDate | undefined): CalendarDate | null {\n  return date?.kind === \"absolute\" ? parseDate(date.date) : null;\n}\n\nconst dayPresets = [0, -1, 1, -7, -30];\n\nexport function FilterDateEditor({\n  labels,\n  locale,\n  onCommit,\n  onValueChange,\n  value,\n  weekStartsOn,\n}: TypedEditorProps) {\n  const date = isFilterDate(value) ? value : undefined;\n  const choices: Choice[] = dayPresets.map((amount) => {\n    const preset: FilterDate = { kind: \"relative\", amount, unit: \"day\" };\n\n    return {\n      id: String(amount),\n      label: capitalize(formatFilterDate(preset, locale)),\n      value: preset,\n    };\n  });\n\n  return (\n    <div data-slot=\"filter-value-editor\" className={filterEditorStackClasses}>\n      <ChoiceList\n        columns\n        choices={choices}\n        label={labels.presets}\n        value={date}\n        onPick={(preset) => {\n          onValueChange(preset);\n          onCommit();\n        }}\n      />\n      <Calendar\n        aria-label={labels.pickDate}\n        locale={locale}\n        weekStartsOn={weekdays[weekStartsOn]}\n        value={toCalendarDate(date)}\n        onValueChange={(next) => {\n          if (next) {\n            onValueChange({ kind: \"absolute\", date: next.toString() });\n            onCommit();\n          }\n        }}\n      />\n    </div>\n  );\n}\n\nexport function FilterDateRangeEditor({\n  labels,\n  locale,\n  onCommit,\n  onValueChange,\n  value,\n  weekStartsOn,\n}: TypedEditorProps) {\n  const [from, to] = Array.isArray(value) ? value : [];\n  const start = isFilterDate(from) ? toCalendarDate(from) : null;\n  const end = isFilterDate(to) ? toCalendarDate(to) : null;\n\n  return (\n    <div data-slot=\"filter-value-editor\" className={filterEditorStackClasses}>\n      <RangeCalendar\n        aria-label={labels.pickRange}\n        locale={locale}\n        weekStartsOn={weekdays[weekStartsOn]}\n        value={start && end ? { start, end } : null}\n        onValueChange={(range) => {\n          if (range) {\n            onValueChange([\n              { kind: \"absolute\", date: range.start.toString() },\n              { kind: \"absolute\", date: range.end.toString() },\n            ]);\n            onCommit();\n          }\n        }}\n      />\n    </div>\n  );\n}\n\nconst durationPresets: FilterDuration[] = [\n  { amount: 7, unit: \"day\" },\n  { amount: 14, unit: \"day\" },\n  { amount: 30, unit: \"day\" },\n  { amount: 3, unit: \"month\" },\n  { amount: 1, unit: \"year\" },\n];\n\nconst units: FilterDateUnit[] = [\"day\", \"week\", \"month\", \"year\"];\n\nexport function FilterDurationEditor({\n  labels,\n  locale,\n  onCommit,\n  onValueChange,\n  value,\n}: TypedEditorProps) {\n  const duration = isFilterDuration(value) ? value : undefined;\n  const [{ amount, unit }, setDraft] = useValueDraft(\n    value,\n    toDurationDraft,\n    fromDurationDraft,\n  );\n\n  const emit = (nextAmount: string, nextUnit: FilterDateUnit) => {\n    const next = { amount: nextAmount, unit: nextUnit };\n\n    setDraft(next);\n    onValueChange(fromDurationDraft(next));\n  };\n\n  return (\n    <div data-slot=\"filter-value-editor\" className={filterEditorStackClasses}>\n      <ChoiceList\n        choices={durationPresets.map((preset) => ({\n          id: `${preset.amount}-${preset.unit}`,\n          label: capitalize(formatFilterDuration(preset, locale)),\n          value: preset,\n        }))}\n        label={labels.presets}\n        value={duration}\n        onPick={(preset) => {\n          onValueChange(preset);\n          onCommit();\n        }}\n      />\n      <div className={rowClasses}>\n        <NumberInput\n          aria-label={labels.amount}\n          controlSize=\"sm\"\n          numberMode=\"numeric\"\n          className=\"w-20\"\n          value={amount}\n          onChange={(event) => {\n            emit(event.currentTarget.value, unit);\n          }}\n          onKeyDown={commitOnEnter(onCommit)}\n        />\n        <select\n          aria-label={labels.unit}\n          value={unit}\n          className={selectClasses}\n          onChange={(event) => {\n            emit(amount, event.currentTarget.value as FilterDateUnit);\n          }}\n        >\n          {units.map((item) => (\n            <option key={item} value={item}>\n              {labels.units[item]}\n            </option>\n          ))}\n        </select>\n      </div>\n    </div>\n  );\n}\n\nconst periods: FilterDate[] = ([\"week\", \"month\", \"year\"] as const).flatMap(\n  (unit) =>\n    [0, -1, 1].map((amount): FilterDate => ({\n      kind: \"relative\",\n      amount,\n      unit,\n    })),\n);\n\nexport function FilterPeriodEditor({\n  labels,\n  locale,\n  onCommit,\n  onValueChange,\n  value,\n}: TypedEditorProps) {\n  const format = new Intl.RelativeTimeFormat(locale, { numeric: \"auto\" });\n\n  return (\n    <div data-slot=\"filter-value-editor\" className={filterEditorStackClasses}>\n      <ChoiceList\n        choices={periods.map((period) => ({\n          id:\n            period.kind === \"relative\" ? `${period.amount}-${period.unit}` : \"\",\n          label:\n            period.kind === \"relative\"\n              ? capitalize(format.format(period.amount, period.unit))\n              : \"\",\n          value: period,\n        }))}\n        label={labels.presets}\n        value={value}\n        onPick={(period) => {\n          onValueChange(period);\n          onCommit();\n        }}\n      />\n    </div>\n  );\n}\n"
    },
    {
      "path": "packages/components/src/components/filter-builder/filter-value-editor.tsx",
      "type": "registry:file",
      "target": "~/components/dethink/components/filter-builder/filter-value-editor.tsx",
      "content": "import {\n  Autocomplete,\n  ListBox,\n  ListBoxItem,\n  useFilter,\n  type Selection,\n} from \"react-aria-components\";\nimport { useRef } from \"react\";\nimport { Input } from \"../input\";\nimport { resolveFilterFieldType } from \"./filter-field-types\";\nimport { getDefaultFilterOperator } from \"./filter-core\";\nimport {\n  CheckIcon,\n  SearchInput,\n  closeOnEmptyEscape,\n  filterEditorStackClasses,\n  filterListBoxClasses,\n  filterListCheckboxClasses,\n  filterListEmptyClasses,\n  filterListIndicatorClasses,\n  filterListItemClassNames,\n  useDeferredFocus,\n} from \"./filter-editors\";\nimport {\n  FilterDateEditor,\n  FilterDateRangeEditor,\n  FilterDurationEditor,\n  FilterNumberEditor,\n  FilterNumberRangeEditor,\n  FilterPeriodEditor,\n  defaultFilterTypedEditorLabels,\n  type FilterTypedEditorLabels,\n  type TypedEditorProps,\n} from \"./filter-typed-editors\";\nimport type {\n  FilterField,\n  FilterOperatorDefinition,\n  FilterValue,\n  FilterWeekday,\n} from \"./filter-types\";\n\nexport interface FilterEditorLabels extends FilterTypedEditorLabels {\n  searchOptions: (fieldLabel: string) => string;\n  noResults: string;\n  textValue: (fieldLabel: string) => string;\n  textPlaceholder: string;\n  /** Screen-reader text for an option's facet count. */\n  facetCount: (count: number) => string;\n}\n\nexport const defaultFilterEditorLabels: FilterEditorLabels = {\n  ...defaultFilterTypedEditorLabels,\n  searchOptions: (fieldLabel) => `Search ${fieldLabel.toLowerCase()}`,\n  noResults: \"No results.\",\n  textValue: (fieldLabel) => `${fieldLabel} value`,\n  textPlaceholder: \"Type a value\",\n  facetCount: (count) => `${count} matching`,\n};\n\nexport interface FilterValueEditorProps<TData = unknown> {\n  field: FilterField<TData>;\n  /** Operator the value is for. Defaults to the field's default operator. */\n  operator?: FilterOperatorDefinition;\n  value?: FilterValue;\n  onValueChange: (value: FilterValue | undefined) => void;\n  /** Called when the user confirms the value, e.g. Enter or a preset. */\n  onCommit?: () => void;\n  /** Closes the surrounding editor, e.g. on Escape. */\n  onClose?: () => void;\n  /** Rows per option value, shown next to options and yes/no. */\n  counts?: ReadonlyMap<string, number>;\n  /** Counts are on their way (e.g. from a server): space is reserved. */\n  countsLoading?: boolean;\n  labels?: Partial<FilterEditorLabels>;\n  locale?: string;\n  weekStartsOn?: FilterWeekday;\n}\n\nfunction Count({\n  count,\n  labels,\n  loading = false,\n}: {\n  count: number | undefined;\n  labels: FilterEditorLabels;\n  loading?: boolean;\n}) {\n  if (loading) {\n    // Same box as a count, so options don't shift when counts arrive.\n    return (\n      <span\n        aria-hidden=\"true\"\n        data-slot=\"filter-facet-count-loading\"\n        className=\"bg-muted ms-auto inline-block h-3 w-6 shrink-0 rounded-sm motion-safe:animate-pulse\"\n      />\n    );\n  }\n\n  if (count === undefined) {\n    return null;\n  }\n\n  return (\n    <span className=\"text-muted-foreground ms-auto min-w-6 shrink-0 text-end text-xs tabular-nums\">\n      <span aria-hidden=\"true\">{count}</span>\n      <span className=\"sr-only\">, {labels.facetCount(count)}</span>\n    </span>\n  );\n}\n\nfunction TextEditor({\n  field,\n  labels,\n  onCommit,\n  onValueChange,\n  value,\n}: {\n  field: FilterField;\n  labels: FilterEditorLabels;\n  onCommit: () => void;\n  onValueChange: (value: FilterValue | undefined) => void;\n  value: FilterValue | undefined;\n}) {\n  const inputRef = useRef<HTMLInputElement>(null);\n\n  useDeferredFocus(inputRef);\n\n  return (\n    <div data-slot=\"filter-value-editor\" className={filterEditorStackClasses}>\n      <Input\n        ref={inputRef}\n        aria-label={labels.textValue(field.label)}\n        controlSize=\"sm\"\n        placeholder={field.placeholder ?? labels.textPlaceholder}\n        value={typeof value === \"string\" ? value : \"\"}\n        onChange={(event) => {\n          const next = event.currentTarget.value;\n\n          onValueChange(next === \"\" ? undefined : next);\n        }}\n        onKeyDown={(event) => {\n          if (event.key === \"Enter\") {\n            event.preventDefault();\n            onCommit();\n          }\n        }}\n      />\n    </div>\n  );\n}\n\nfunction OptionsEditor({\n  counts,\n  countsLoading,\n  field,\n  labels,\n  onClose,\n  onValueChange,\n  value,\n}: {\n  counts?: ReadonlyMap<string, number>;\n  countsLoading?: boolean;\n  field: FilterField;\n  labels: FilterEditorLabels;\n  onClose?: () => void;\n  onValueChange: (value: FilterValue | undefined) => void;\n  value: FilterValue | undefined;\n}) {\n  const { contains } = useFilter({ sensitivity: \"base\" });\n  const options = field.options ?? [];\n  const selected = Array.isArray(value) ? value.map(String) : [];\n\n  return (\n    <div\n      data-slot=\"filter-value-editor\"\n      className={filterEditorStackClasses}\n      onKeyDownCapture={closeOnEmptyEscape(onClose)}\n    >\n      <Autocomplete filter={contains}>\n        <SearchInput label={labels.searchOptions(field.label)} />\n        <ListBox\n          aria-label={field.label}\n          items={options.map((option) => ({\n            id: option.value,\n            label: option.label,\n            keywords: option.keywords,\n          }))}\n          selectionMode=\"multiple\"\n          selectedKeys={selected}\n          className={filterListBoxClasses}\n          renderEmptyState={() => (\n            <div className={filterListEmptyClasses}>{labels.noResults}</div>\n          )}\n          onSelectionChange={(keys: Selection) => {\n            const next = options\n              .map((option) => option.value)\n              .filter((optionValue) => keys === \"all\" || keys.has(optionValue));\n\n            onValueChange(next.length > 0 ? next : undefined);\n          }}\n        >\n          {(item) => (\n            <ListBoxItem\n              id={item.id}\n              textValue={[item.label, ...(item.keywords ?? [])].join(\" \")}\n              data-empty={counts && !counts.get(item.id) ? \"\" : undefined}\n              className={filterListItemClassNames({\n                className:\n                  \"group data-[empty]:text-muted-foreground grid-cols-[1rem_minmax(0,1fr)_auto]\",\n              })}\n            >\n              <span aria-hidden=\"true\" className={filterListCheckboxClasses}>\n                <CheckIcon />\n              </span>\n              <span className=\"min-w-0 truncate\">{item.label}</span>\n              <Count\n                count={counts ? (counts.get(item.id) ?? 0) : undefined}\n                labels={labels}\n                loading={countsLoading}\n              />\n            </ListBoxItem>\n          )}\n        </ListBox>\n      </Autocomplete>\n    </div>\n  );\n}\n\nfunction BooleanEditor({\n  counts,\n  countsLoading,\n  field,\n  labels,\n  onCommit,\n  onValueChange,\n  value,\n}: {\n  counts?: ReadonlyMap<string, number>;\n  countsLoading?: boolean;\n  field: FilterField;\n  labels: FilterEditorLabels;\n  onCommit: () => void;\n  onValueChange: (value: FilterValue | undefined) => void;\n  value: FilterValue | undefined;\n}) {\n  const items = [\n    { id: \"true\", label: field.trueLabel ?? \"Yes\", value: true },\n    { id: \"false\", label: field.falseLabel ?? \"No\", value: false },\n  ];\n\n  return (\n    <div data-slot=\"filter-value-editor\" className={filterEditorStackClasses}>\n      <ListBox\n        aria-label={field.label}\n        // eslint-disable-next-line jsx-a11y/no-autofocus -- Opened on demand.\n        autoFocus=\"first\"\n        items={items}\n        className={filterListBoxClasses}\n        onAction={(key) => {\n          onValueChange(key === \"true\");\n          onCommit();\n        }}\n      >\n        {(item) => (\n          <ListBoxItem\n            id={item.id}\n            textValue={item.label}\n            className={filterListItemClassNames({\n              className: \"grid-cols-[1rem_minmax(0,1fr)_auto]\",\n            })}\n          >\n            <span aria-hidden=\"true\" className={filterListIndicatorClasses}>\n              {value === item.value ? <CheckIcon /> : null}\n            </span>\n            <span className=\"min-w-0 truncate\">{item.label}</span>\n            <Count\n              count={counts ? (counts.get(item.id) ?? 0) : undefined}\n              labels={labels}\n              loading={countsLoading}\n            />\n          </ListBoxItem>\n        )}\n      </ListBox>\n    </div>\n  );\n}\n\nconst typedEditors: Record<\n  string,\n  (props: TypedEditorProps) => React.JSX.Element\n> = {\n  number: FilterNumberEditor,\n  numberRange: FilterNumberRangeEditor,\n  date: FilterDateEditor,\n  dateRange: FilterDateRangeEditor,\n  duration: FilterDurationEditor,\n  period: FilterPeriodEditor,\n};\n\n/**\n * Value editor for a condition, chosen by the operator's value kind: text\n * box, option checklist (with facet counts), yes/no, number or number range,\n * date presets and calendar, date range, \"last N\" durations or calendar\n * periods. Custom field types render their own editor.\n */\nexport function FilterValueEditor<TData>({\n  counts,\n  countsLoading = false,\n  field: typedField,\n  labels: labelOverrides,\n  locale = \"en-US\",\n  onClose,\n  onCommit = () => undefined,\n  onValueChange,\n  operator: operatorProp,\n  value,\n  weekStartsOn = 1,\n}: FilterValueEditorProps<TData>) {\n  const field = typedField as FilterField;\n  const labels = { ...defaultFilterEditorLabels, ...labelOverrides };\n  const operator = operatorProp ?? getDefaultFilterOperator(field);\n  const type = resolveFilterFieldType(field);\n\n  if (type.renderEditor && operator) {\n    return (\n      <div data-slot=\"filter-value-editor\" className={filterEditorStackClasses}>\n        {type.renderEditor({ field, operator, value, onValueChange, onCommit })}\n      </div>\n    );\n  }\n\n  const kind = operator?.valueKind;\n  const Typed = kind ? typedEditors[kind] : undefined;\n\n  if (Typed) {\n    return (\n      <Typed\n        // A new operator kind needs fresh drafts.\n        key={kind}\n        fieldLabel={field.label}\n        value={value}\n        onValueChange={onValueChange}\n        onCommit={onCommit}\n        labels={labels}\n        locale={locale}\n        weekStartsOn={weekStartsOn}\n      />\n    );\n  }\n\n  if (kind === \"list\") {\n    return (\n      <OptionsEditor\n        counts={counts}\n        countsLoading={countsLoading}\n        field={field}\n        labels={labels}\n        onClose={onClose}\n        onValueChange={onValueChange}\n        value={value}\n      />\n    );\n  }\n\n  if (kind === \"boolean\") {\n    return (\n      <BooleanEditor\n        counts={counts}\n        countsLoading={countsLoading}\n        field={field}\n        labels={labels}\n        onCommit={onCommit}\n        onValueChange={onValueChange}\n        value={value}\n      />\n    );\n  }\n\n  return (\n    <TextEditor\n      field={field}\n      labels={labels}\n      onCommit={onCommit}\n      onValueChange={onValueChange}\n      value={value}\n    />\n  );\n}\n"
    },
    {
      "path": "packages/components/src/components/filter-builder/filter-bar-parts.tsx",
      "type": "registry:file",
      "target": "~/components/dethink/components/filter-builder/filter-bar-parts.tsx",
      "content": "import {\n  createContext,\n  useContext,\n  useEffect,\n  useRef,\n  type KeyboardEvent,\n} from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport {\n  getFilterOperatorLabel,\n  type DescribeFilterOptions,\n  type FilterFacetTarget,\n  type FilterRescue,\n} from \"./filter-core\";\nimport type {\n  FilterEvaluateOptions,\n  FilterFields,\n  FilterOperatorDefinition,\n  FilterValue,\n} from \"./filter-types\";\nimport {\n  defaultFilterEditorLabels,\n  type FilterEditorLabels,\n} from \"./filter-value-editor\";\nimport type { FilterState } from \"./use-filter-state\";\n\nexport type FilterBarSize = \"sm\" | \"md\";\n\nexport interface FilterBarLabels extends FilterEditorLabels {\n  toolbar: string;\n  add: string;\n  addFirst: string;\n  addMenu: string;\n  back: string;\n  clear: string;\n  undo: string;\n  searchFields: string;\n  fieldList: string;\n  operatorList: string;\n  selectValue: string;\n  changeField: (fieldLabel: string) => string;\n  changeOperator: (operatorLabel: string) => string;\n  changeValue: (valueText: string) => string;\n  remove: (description: string) => string;\n  editor: (description: string) => string;\n  showMore: (hiddenCount: number) => string;\n  showLess: string;\n  resultCount: (count: number) => string;\n  unknownField: (fieldKey: string) => string;\n  and: string;\n  or: string;\n  not: string;\n  /** Shown before the chips when the whole filter is negated. */\n  notMatching: string;\n  missingValue: string;\n  empty: string;\n  advanced: string;\n  groupEditor: string;\n  editGroup: (description: string) => string;\n  emptyGroup: string;\n  matchAll: string;\n  matchAny: string;\n  matchPrefix: string;\n  matchSuffix: string;\n  negate: string;\n  addCondition: string;\n  addGroup: string;\n  wrapInGroup: string;\n  ungroup: string;\n  removeGroup: string;\n  moveUp: string;\n  moveDown: string;\n  nodeActions: (description: string) => string;\n  maxDepthReached: (maxDepth: number) => string;\n  /**\n   * Screen-reader text for a chip's impact count: positive when the chip\n   * removes rows, negative when it adds them (an OR branch).\n   */\n  impact: (count: number) => string;\n  relax: string;\n  rescue: (description: string, count: number) => string;\n  operator: (\n    operator: FilterOperatorDefinition,\n    value: FilterValue | undefined,\n  ) => string;\n}\n\nexport const defaultFilterBarLabels: FilterBarLabels = {\n  ...defaultFilterEditorLabels,\n  toolbar: \"Filters\",\n  add: \"Filter\",\n  addFirst: \"Add filter\",\n  addMenu: \"Add a filter\",\n  back: \"Back to fields\",\n  clear: \"Clear\",\n  undo: \"Undo filter change\",\n  searchFields: \"Filter by…\",\n  fieldList: \"Fields\",\n  operatorList: \"Operators\",\n  selectValue: \"Select…\",\n  changeField: (fieldLabel) => `Change field, ${fieldLabel}`,\n  changeOperator: (operatorLabel) => `Change operator, ${operatorLabel}`,\n  changeValue: (valueText) => `Change value, ${valueText}`,\n  remove: (description) => `Remove filter, ${description}`,\n  editor: (description) => `Edit filter, ${description}`,\n  showMore: (hiddenCount) => `+${hiddenCount} more`,\n  showLess: \"Show fewer\",\n  resultCount: (count) => `${count} ${count === 1 ? \"result\" : \"results\"}`,\n  unknownField: (fieldKey) => `Unknown field ${fieldKey}`,\n  and: \"and\",\n  or: \"or\",\n  not: \"not\",\n  notMatching: \"Not matching\",\n  missingValue: \"(no value)\",\n  empty: \"No filters\",\n  advanced: \"Advanced\",\n  groupEditor: \"Edit filter groups\",\n  editGroup: (description) => `Edit group, ${description}`,\n  emptyGroup: \"Empty group\",\n  matchAll: \"all\",\n  matchAny: \"any\",\n  matchPrefix: \"Match\",\n  matchSuffix: \"of the following\",\n  negate: \"Not\",\n  addCondition: \"Condition\",\n  addGroup: \"Group\",\n  wrapInGroup: \"Wrap in group\",\n  ungroup: \"Ungroup\",\n  removeGroup: \"Remove group\",\n  moveUp: \"Move up\",\n  moveDown: \"Move down\",\n  nodeActions: (description) => `Actions for ${description}`,\n  maxDepthReached: (maxDepth) =>\n    `Groups can be nested ${maxDepth} levels deep.`,\n  operator: getFilterOperatorLabel,\n  impact: (count) => {\n    const rows = Math.abs(count);\n\n    return `${count < 0 ? \"adds\" : \"removes\"} ${rows} ${rows === 1 ? \"row\" : \"rows\"}`;\n  },\n  relax: \"Relax\",\n  rescue: (description, count) =>\n    `No results. Removing “${description}” would show ${count}.`,\n};\n\nexport interface FilterBarContextValue {\n  // A plain mutable ref: React 18's RefObject is read-only.\n  addButtonRef: { current: HTMLButtonElement | null };\n  addMenuOpen: boolean;\n  collapseAfter: number;\n  describeOptions: DescribeFilterOptions;\n  evaluateOptions: FilterEvaluateOptions;\n  expanded: boolean;\n  /** Rows per value of a field, applying the other conditions. */\n  getFacets?: (\n    fieldKey: string,\n    target?: FilterFacetTarget,\n  ) => ReadonlyMap<string, number> | undefined;\n  /** Whether counts for a field are loading, from server facets. */\n  getFacetStatus?: (\n    fieldKey: string,\n    target?: FilterFacetTarget,\n  ) => \"loading\" | \"ready\" | \"error\" | \"unsupported\";\n  /**\n   * Rows removed by each active condition, keyed by condition id. Negative\n   * when the condition adds rows (an OR branch).\n   */\n  impact?: ReadonlyMap<string, number>;\n  /** Most restrictive condition when nothing matches. */\n  rescue?: FilterRescue;\n  fields: FilterFields;\n  labels: FilterBarLabels;\n  locale: string;\n  maxDepth: number;\n  requestFocus: (element: HTMLElement | null | undefined) => void;\n  setAddMenuOpen: (open: boolean) => void;\n  setExpanded: (expanded: boolean) => void;\n  size: FilterBarSize;\n  state: FilterState;\n}\n\nexport const FilterBarContext = createContext<FilterBarContextValue | null>(\n  null,\n);\n\nexport function useFilterBarContext(part: string) {\n  const context = useContext(FilterBarContext);\n\n  if (!context) {\n    throw new Error(`${part} must be rendered inside <FilterBar>.`);\n  }\n\n  return context;\n}\n\n/** Filter state and labels of the nearest FilterBar. */\nexport function useFilterBar() {\n  const { fields, labels, state } = useFilterBarContext(\"useFilterBar\");\n\n  return { fields, labels, state };\n}\n\nexport const ITEM_SELECTOR = \"[data-filter-bar-item]\";\n\nexport const filterBarClasses =\n  \"@container grid min-w-0 gap-[var(--dt-space-2)]\";\n\nexport const filterBarToolbarClasses =\n  \"flex min-w-0 flex-wrap items-center gap-[var(--dt-space-2)]\";\n\nexport const chipBaseClasses =\n  \"inline-flex min-w-0 max-w-full items-stretch overflow-hidden rounded-md border border-border bg-background text-foreground shadow-xs @max-xl:data-[overflow]:hidden data-[incomplete]:border-dashed\";\n\nexport const chipSizeClasses: Record<FilterBarSize, string> = {\n  sm: \"h-7 text-xs\",\n  md: \"h-8 text-sm\",\n};\n\nexport const segmentClasses =\n  \"inline-flex min-w-0 items-center gap-[var(--dt-space-1)] px-[var(--dt-space-2)] outline-none [&+&]:border-s [&+&]:border-border motion-safe:transition-colors motion-safe:duration-[var(--dt-motion-fast)] hover:bg-muted focus-visible:bg-muted focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring aria-expanded:bg-muted\";\n\nexport const segmentFieldClasses = \"shrink-0 font-medium\";\n\nexport const segmentOperatorClasses = \"shrink-0 text-muted-foreground\";\n\nexport const segmentValueClasses =\n  \"max-w-[16rem] data-[incomplete]:italic data-[incomplete]:text-muted-foreground\";\n\nexport const segmentRemoveClasses =\n  \"shrink-0 justify-center px-[var(--dt-space-1-5)] text-muted-foreground hover:text-foreground\";\n\nexport const groupChipTextClasses =\n  \"inline-flex min-w-0 items-center px-[var(--dt-space-2)] text-muted-foreground\";\n\nexport const actionBaseClasses =\n  \"inline-flex shrink-0 items-center gap-[var(--dt-space-1-5)] rounded-md px-[var(--dt-space-2-5)] font-medium outline-none motion-safe:transition-colors motion-safe:duration-[var(--dt-motion-fast)] focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\";\n\nexport const addButtonClasses =\n  \"border border-dashed border-border text-muted-foreground hover:border-solid hover:bg-muted hover:text-foreground aria-expanded:border-solid aria-expanded:bg-muted aria-expanded:text-foreground\";\n\nexport const ghostActionClasses =\n  \"text-muted-foreground hover:bg-muted hover:text-foreground\";\n\nexport const moreButtonClasses = `${ghostActionClasses} hidden @max-xl:inline-flex`;\n\nexport const negationClasses =\n  \"inline-flex shrink-0 items-center self-stretch rounded-sm bg-muted px-[var(--dt-space-1-5)] text-xs font-semibold uppercase text-foreground\";\n\nexport const conjunctionClasses =\n  \"select-none text-xs font-medium uppercase text-muted-foreground\";\n\nexport const editorHeaderClasses =\n  \"flex min-w-0 items-center gap-[var(--dt-space-1)] text-sm\";\n\nexport const editorBackClasses =\n  \"inline-flex size-7 shrink-0 items-center justify-center rounded-sm text-muted-foreground outline-none hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\";\n\nexport const editorPopoverClasses =\n  \"w-max min-w-[var(--dt-filter-editor-width,16rem)] p-[var(--dt-space-2)]\";\n\nexport function filterBarClassNames({\n  className,\n}: { className?: string } = {}) {\n  return cn(filterBarClasses, className);\n}\n\nexport function filterChipClassNames({\n  className,\n  size = \"md\",\n}: { className?: string; size?: FilterBarSize } = {}) {\n  return cn(chipBaseClasses, chipSizeClasses[size], className);\n}\n\nexport function filterBarActionClassNames({\n  className,\n  size = \"md\",\n  variant = \"ghost\",\n}: {\n  className?: string;\n  size?: FilterBarSize;\n  variant?: \"add\" | \"ghost\";\n} = {}) {\n  return cn(\n    actionBaseClasses,\n    chipSizeClasses[size],\n    variant === \"add\" ? addButtonClasses : ghostActionClasses,\n    className,\n  );\n}\n\nexport function PlusIcon() {\n  return (\n    <svg aria-hidden=\"true\" viewBox=\"0 0 16 16\" className=\"size-4 shrink-0\">\n      <path\n        d=\"M8 3.5v9M3.5 8h9\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeLinecap=\"round\"\n        strokeWidth=\"1.5\"\n      />\n    </svg>\n  );\n}\n\nexport function CloseIcon() {\n  return (\n    <svg aria-hidden=\"true\" viewBox=\"0 0 16 16\" className=\"size-3.5 shrink-0\">\n      <path\n        d=\"m4.5 4.5 7 7m0-7-7 7\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeLinecap=\"round\"\n        strokeWidth=\"1.5\"\n      />\n    </svg>\n  );\n}\n\nexport function UndoIcon() {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      viewBox=\"0 0 16 16\"\n      className=\"size-4 shrink-0 rtl:-scale-x-100\"\n    >\n      <path\n        d=\"M5.5 4 2.5 7l3 3M3 7h6.5a3.5 3.5 0 0 1 0 7H8\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        strokeWidth=\"1.5\"\n      />\n    </svg>\n  );\n}\n\nexport function BackIcon() {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      viewBox=\"0 0 16 16\"\n      className=\"size-4 shrink-0 rtl:-scale-x-100\"\n    >\n      <path\n        d=\"M10 3.5 5.5 8l4.5 4.5\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        strokeWidth=\"1.5\"\n      />\n    </svg>\n  );\n}\n\nexport function isVisible(element: HTMLElement) {\n  return typeof element.checkVisibility === \"function\"\n    ? element.checkVisibility()\n    : true;\n}\n\nexport function isEditableTarget(target: EventTarget | null) {\n  return (\n    target instanceof HTMLElement &&\n    (target.isContentEditable ||\n      target.tagName === \"INPUT\" ||\n      target.tagName === \"TEXTAREA\" ||\n      target.tagName === \"SELECT\")\n  );\n}\n\nexport function formatChipValue(values: string[], placeholder: string) {\n  if (values.length === 0) {\n    return placeholder;\n  }\n\n  if (values.length <= 2) {\n    return values.join(\", \");\n  }\n\n  return `${values[0]}, ${values[1]} +${values.length - 2}`;\n}\n\n/** Restores focus to the segment that opened an editor once it closes. */\nexport function useEditorFocusReturn(open: boolean) {\n  const returnRef = useRef<HTMLElement | null>(null);\n  const wasOpenRef = useRef(open);\n\n  useEffect(() => {\n    const wasOpen = wasOpenRef.current;\n\n    wasOpenRef.current = open;\n\n    if (!wasOpen || open) {\n      return undefined;\n    }\n\n    const timer = window.setTimeout(() => {\n      const target = returnRef.current;\n\n      if (target?.isConnected) {\n        target.focus();\n      }\n    }, 0);\n\n    return () => {\n      window.clearTimeout(timer);\n    };\n  }, [open]);\n\n  return returnRef;\n}\n\n/** The toolbar item to focus after a chip is removed: next, else previous. */\nexport function getNeighborItem(chip: HTMLElement | null) {\n  // The toolbar and each group editor mark their own focus scope.\n  const scope = chip?.closest<HTMLElement>(\"[data-filter-focus-scope]\");\n\n  if (!chip || !scope) {\n    return undefined;\n  }\n\n  const items = [...scope.querySelectorAll<HTMLElement>(ITEM_SELECTOR)].filter(\n    (item) =>\n      isVisible(item) &&\n      !chip.contains(item) &&\n      item.closest(\"[data-filter-focus-scope]\") === scope,\n  );\n  const after = items.find(\n    (item) =>\n      chip.compareDocumentPosition(item) & Node.DOCUMENT_POSITION_FOLLOWING,\n  );\n\n  return (\n    after ??\n    [...items]\n      .reverse()\n      .find(\n        (item) =>\n          chip.compareDocumentPosition(item) & Node.DOCUMENT_POSITION_PRECEDING,\n      )\n  );\n}\n\n/**\n * Backspace/Delete on one of this chip's own items. Checked against the DOM\n * because React bubbles keys out of portaled editors (a group editor's chips)\n * to the chip that opened them.\n */\nexport function isRemoveKey(event: KeyboardEvent<HTMLElement>) {\n  return (\n    (event.key === \"Backspace\" || event.key === \"Delete\") &&\n    event.target instanceof HTMLElement &&\n    event.target.hasAttribute(\"data-filter-bar-item\") &&\n    event.currentTarget.contains(event.target)\n  );\n}\n"
    },
    {
      "path": "packages/components/src/components/filter-builder/filter-chip.tsx",
      "type": "registry:file",
      "target": "~/components/dethink/components/filter-builder/filter-chip.tsx",
      "content": "import {\n  useId,\n  useRef,\n  useState,\n  type HTMLAttributes,\n  type KeyboardEvent,\n} from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { Popover, PopoverContent } from \"../popover\";\nimport {\n  describeFilterCondition,\n  formatFilterValue,\n  getDefaultFilterOperator,\n  getFilterField,\n  getFilterOperator,\n  isFilterConditionActive,\n} from \"./filter-core\";\nimport { FilterFieldPicker, FilterOperatorPicker } from \"./filter-editors\";\nimport { FilterValueEditor } from \"./filter-value-editor\";\nimport type { FilterCondition } from \"./filter-types\";\nimport {\n  CloseIcon,\n  editorPopoverClasses,\n  filterChipClassNames,\n  formatChipValue,\n  getNeighborItem,\n  isRemoveKey,\n  negationClasses,\n  segmentClasses,\n  segmentFieldClasses,\n  segmentOperatorClasses,\n  segmentRemoveClasses,\n  segmentValueClasses,\n  useEditorFocusReturn,\n  useFilterBarContext,\n} from \"./filter-bar-parts\";\n\ntype ChipSegment = \"field\" | \"operator\" | \"value\";\n\nexport interface FilterChipProps extends Omit<\n  HTMLAttributes<HTMLDivElement>,\n  \"children\"\n> {\n  condition: FilterCondition;\n  /** Position among the root chips, used for narrow-width collapsing. */\n  index?: number;\n}\n\n/** A condition rendered as `Field | operator | value | ×` segments. */\nexport function FilterChip({\n  className,\n  condition,\n  index = 0,\n  onKeyDown,\n  ...props\n}: FilterChipProps) {\n  const {\n    collapseAfter,\n    describeOptions,\n    evaluateOptions,\n    expanded,\n    fields,\n    getFacetStatus,\n    getFacets,\n    impact,\n    labels,\n    locale,\n    requestFocus,\n    size,\n    state,\n  } = useFilterBarContext(\"FilterChip\");\n  const impactId = useId();\n  const [editing, setEditing] = useState<ChipSegment | null>(null);\n  const [session, setSession] = useState(0);\n  // The same condition can render in the bar and in a group editor at once,\n  // so each chip instance keeps its own undo sessions.\n  const instanceId = useId();\n  const chipRef = useRef<HTMLDivElement>(null);\n  const returnFocusRef = useEditorFocusReturn(editing !== null);\n  const field = getFilterField(fields, condition.field);\n  const operator = field\n    ? getFilterOperator(field, condition.operator)\n    : undefined;\n  const description = describeFilterCondition(\n    condition,\n    fields,\n    describeOptions,\n  );\n  const active = isFilterConditionActive(condition, fields);\n  const coalesceKey = `${instanceId}:${condition.id}:${session}`;\n  const overflow = index >= collapseAfter && !expanded;\n\n  const open = (segment: ChipSegment, trigger: HTMLElement) => {\n    returnFocusRef.current = trigger;\n    setSession((current) => current + 1);\n    setEditing(segment);\n  };\n\n  const remove = () => {\n    requestFocus(getNeighborItem(chipRef.current));\n    state.removeNode(condition.id);\n  };\n\n  const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n    onKeyDown?.(event);\n\n    if (event.defaultPrevented) {\n      return;\n    }\n\n    if (isRemoveKey(event)) {\n      event.preventDefault();\n      remove();\n    }\n  };\n\n  if (!field || !operator) {\n    return (\n      // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- Keys delegate to the chip's roving segment buttons.\n      <div\n        {...props}\n        ref={chipRef}\n        role=\"group\"\n        aria-label={description}\n        data-slot=\"filter-chip\"\n        data-invalid=\"\"\n        data-overflow={overflow ? \"\" : undefined}\n        className={filterChipClassNames({\n          size,\n          className: cn(\"border-destructive/60\", className),\n        })}\n        onKeyDown={handleKeyDown}\n      >\n        <span className={cn(segmentClasses, \"hover:bg-transparent\")}>\n          {labels.unknownField(condition.field)}\n        </span>\n        <button\n          type=\"button\"\n          data-filter-bar-item=\"remove\"\n          data-slot=\"filter-chip-remove\"\n          aria-label={labels.remove(description)}\n          className={cn(segmentClasses, segmentRemoveClasses)}\n          onClick={remove}\n        >\n          <CloseIcon />\n        </button>\n      </div>\n    );\n  }\n\n  const values = formatFilterValue(field, condition.value, {\n    locale,\n    operator,\n  });\n  const impactCount = impact?.get(condition.id);\n  const valueText = formatChipValue(values, labels.selectValue);\n  const operatorLabel = labels.operator(operator, condition.value);\n\n  const update = (patch: Partial<Omit<FilterCondition, \"id\" | \"type\">>) => {\n    state.updateCondition(condition.id, patch, { coalesceKey });\n  };\n\n  return (\n    // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- Keys delegate to the chip's roving segment buttons.\n    <div\n      {...props}\n      ref={chipRef}\n      role=\"group\"\n      aria-label={description}\n      data-slot=\"filter-chip\"\n      aria-describedby={impactCount ? impactId : undefined}\n      data-active={active ? \"\" : undefined}\n      data-incomplete={active ? undefined : \"\"}\n      data-negated={condition.not ? \"\" : undefined}\n      data-overflow={overflow ? \"\" : undefined}\n      className={filterChipClassNames({ size, className })}\n      onKeyDown={handleKeyDown}\n    >\n      {condition.not ? (\n        <span\n          data-slot=\"filter-chip-not\"\n          className={cn(negationClasses, \"rounded-none\")}\n        >\n          {labels.not}\n        </span>\n      ) : null}\n      <button\n        type=\"button\"\n        data-filter-bar-item=\"field\"\n        data-slot=\"filter-chip-field\"\n        aria-label={labels.changeField(field.label)}\n        aria-haspopup=\"dialog\"\n        aria-expanded={editing === \"field\"}\n        className={cn(segmentClasses, segmentFieldClasses)}\n        onClick={(event) => {\n          open(\"field\", event.currentTarget);\n        }}\n      >\n        {field.label}\n      </button>\n      <button\n        type=\"button\"\n        data-filter-bar-item=\"operator\"\n        data-slot=\"filter-chip-operator\"\n        aria-label={labels.changeOperator(operatorLabel)}\n        aria-haspopup=\"dialog\"\n        aria-expanded={editing === \"operator\"}\n        className={cn(segmentClasses, segmentOperatorClasses)}\n        onClick={(event) => {\n          open(\"operator\", event.currentTarget);\n        }}\n      >\n        {operatorLabel}\n      </button>\n      {operator.arity === \"none\" ? null : (\n        <button\n          type=\"button\"\n          data-filter-bar-item=\"value\"\n          data-slot=\"filter-chip-value\"\n          data-incomplete={values.length === 0 ? \"\" : undefined}\n          aria-label={labels.changeValue(\n            values.length > 0 ? values.join(\", \") : labels.selectValue,\n          )}\n          aria-haspopup=\"dialog\"\n          aria-expanded={editing === \"value\"}\n          className={cn(segmentClasses, segmentValueClasses)}\n          onClick={(event) => {\n            open(\"value\", event.currentTarget);\n          }}\n        >\n          <span className=\"min-w-0 truncate\">{valueText}</span>\n        </button>\n      )}\n      {impactCount ? (\n        <span\n          data-slot=\"filter-chip-impact\"\n          className=\"text-muted-foreground inline-flex shrink-0 items-center px-[var(--dt-space-1-5)] text-xs tabular-nums\"\n        >\n          <span aria-hidden=\"true\">\n            {impactCount > 0 ? `−${impactCount}` : `+${-impactCount}`}\n          </span>\n          <span id={impactId} className=\"sr-only\">\n            {labels.impact(impactCount)}\n          </span>\n        </span>\n      ) : null}\n      <button\n        type=\"button\"\n        data-filter-bar-item=\"remove\"\n        data-slot=\"filter-chip-remove\"\n        aria-label={labels.remove(description)}\n        className={cn(segmentClasses, segmentRemoveClasses)}\n        onClick={remove}\n      >\n        <CloseIcon />\n      </button>\n      <Popover\n        anchorRef={chipRef}\n        open={editing !== null}\n        onOpenChange={(nextOpen) => {\n          if (!nextOpen) {\n            setEditing(null);\n          }\n        }}\n      >\n        <PopoverContent\n          aria-label={labels.editor(description)}\n          placement=\"bottom start\"\n          className={editorPopoverClasses}\n        >\n          {editing === \"field\" ? (\n            <FilterFieldPicker\n              fields={fields}\n              selectedKey={field.key}\n              searchLabel={labels.searchFields}\n              listLabel={labels.fieldList}\n              emptyLabel={labels.noResults}\n              onClose={() => {\n                setEditing(null);\n              }}\n              onSelect={(fieldKey) => {\n                const nextField = getFilterField(fields, fieldKey);\n\n                if (!nextField || nextField.key === field.key) {\n                  setEditing(null);\n                  return;\n                }\n\n                const nextOperator = getDefaultFilterOperator(nextField);\n\n                update({\n                  field: nextField.key,\n                  operator: nextOperator?.id ?? condition.operator,\n                  value: undefined,\n                });\n                setEditing(nextOperator?.arity === \"none\" ? null : \"value\");\n              }}\n            />\n          ) : null}\n          {editing === \"operator\" ? (\n            <FilterOperatorPicker\n              field={field}\n              selectedKey={operator.id}\n              value={condition.value}\n              listLabel={labels.operatorList}\n              getOperatorLabel={labels.operator}\n              onSelect={(operatorId) => {\n                const next = getFilterOperator(field, operatorId);\n\n                if (!next || next.id === operator.id) {\n                  setEditing(null);\n                  return;\n                }\n\n                // A different value shape (e.g. a date to \"last 7 days\")\n                // can't keep the old value, so clear it and ask for one.\n                const reshape = next.valueKind !== operator.valueKind;\n\n                update(\n                  reshape\n                    ? { operator: next.id, value: undefined }\n                    : { operator: next.id },\n                );\n                setEditing(reshape && next.arity !== \"none\" ? \"value\" : null);\n              }}\n            />\n          ) : null}\n          {editing === \"value\" ? (\n            <FilterValueEditor\n              field={field}\n              operator={operator}\n              value={condition.value}\n              counts={getFacets?.(field.key, {\n                conditionId: condition.id,\n                operator: operator.id,\n              })}\n              countsLoading={\n                getFacetStatus?.(field.key, {\n                  conditionId: condition.id,\n                  operator: operator.id,\n                }) === \"loading\"\n              }\n              labels={labels}\n              locale={locale}\n              weekStartsOn={evaluateOptions.weekStartsOn}\n              onValueChange={(value) => {\n                update({ value });\n              }}\n              onCommit={() => {\n                setEditing(null);\n              }}\n              onClose={() => {\n                setEditing(null);\n              }}\n            />\n          ) : null}\n        </PopoverContent>\n      </Popover>\n    </div>\n  );\n}\n"
    },
    {
      "path": "packages/components/src/components/filter-builder/filter-add-menu.tsx",
      "type": "registry:file",
      "target": "~/components/dethink/components/filter-builder/filter-add-menu.tsx",
      "content": "import { useId, useRef, useState, type ReactNode } from \"react\";\nimport { Popover, PopoverContent } from \"../popover\";\nimport {\n  createFilterCondition,\n  createFilterId,\n  findFilterNode,\n  getDefaultFilterOperator,\n  getFilterField,\n  isFilterConditionActive,\n} from \"./filter-core\";\nimport { FilterFieldPicker } from \"./filter-editors\";\nimport { FilterValueEditor } from \"./filter-value-editor\";\nimport {\n  BackIcon,\n  PlusIcon,\n  editorBackClasses,\n  editorHeaderClasses,\n  editorPopoverClasses,\n  filterBarActionClassNames,\n  useFilterBarContext,\n} from \"./filter-bar-parts\";\n\nexport interface FilterAddMenuProps {\n  className?: string;\n  /** Button content. Defaults to a plus icon and the add label. */\n  children?: ReactNode;\n  /**\n   * Group that receives the new condition. Defaults to the root, where the\n   * menu is the bar's primary add menu (shared with the add shortcut).\n   */\n  parentId?: string;\n}\n\ninterface AddDraft {\n  conditionId: string;\n  fieldKey: string;\n  session: number;\n}\n\n/** \"+ Filter\" button: pick a field, then a value, in one popover. */\nexport function FilterAddMenu({\n  children,\n  className,\n  parentId,\n}: FilterAddMenuProps) {\n  const context = useFilterBarContext(\"FilterAddMenu\");\n  const {\n    evaluateOptions,\n    fields,\n    getFacetStatus,\n    getFacets,\n    labels,\n    locale,\n    size,\n    state,\n  } = context;\n  const isPrimary = parentId === undefined;\n  const [localOpen, setLocalOpen] = useState(false);\n  const localButtonRef = useRef<HTMLButtonElement>(null);\n  const addButtonRef = isPrimary ? context.addButtonRef : localButtonRef;\n  const addMenuOpen = isPrimary ? context.addMenuOpen : localOpen;\n  const setAddMenuOpen = isPrimary ? context.setAddMenuOpen : setLocalOpen;\n  const instanceId = useId();\n  const [draft, setDraft] = useState<AddDraft | null>(null);\n  const sessionRef = useRef(0);\n  const field = draft ? getFilterField(fields, draft.fieldKey) : undefined;\n  const operator = field ? getDefaultFilterOperator(field) : undefined;\n  const existing = draft\n    ? findFilterNode(state.filter, draft.conditionId)\n    : undefined;\n  const condition = existing?.type === \"condition\" ? existing : undefined;\n  const coalesceKey = draft ? `add:${instanceId}:${draft.session}` : undefined;\n  const hasChips = state.filter.children.length > 0;\n\n  const discardIncomplete = () => {\n    if (condition && !isFilterConditionActive(condition, fields)) {\n      state.removeNode(condition.id, { coalesceKey });\n    }\n  };\n\n  const close = () => {\n    discardIncomplete();\n    setDraft(null);\n    setAddMenuOpen(false);\n  };\n\n  return (\n    <>\n      <button\n        ref={addButtonRef}\n        type=\"button\"\n        data-filter-bar-item=\"add\"\n        data-slot=\"filter-add-menu-trigger\"\n        aria-haspopup=\"dialog\"\n        aria-expanded={addMenuOpen}\n        className={filterBarActionClassNames({\n          size,\n          variant: \"add\",\n          className,\n        })}\n        onClick={() => {\n          setAddMenuOpen(!addMenuOpen);\n        }}\n      >\n        {children ?? (\n          <>\n            <PlusIcon />\n            {hasChips ? labels.add : labels.addFirst}\n          </>\n        )}\n      </button>\n      <Popover\n        anchorRef={addButtonRef}\n        open={addMenuOpen}\n        onOpenChange={(nextOpen) => {\n          if (nextOpen) {\n            setAddMenuOpen(true);\n          } else {\n            close();\n          }\n        }}\n      >\n        <PopoverContent\n          aria-label={labels.addMenu}\n          placement=\"bottom start\"\n          className={editorPopoverClasses}\n        >\n          {field && operator && draft ? (\n            <div className=\"grid min-w-0 gap-[var(--dt-space-2)]\">\n              <div className={editorHeaderClasses}>\n                <button\n                  type=\"button\"\n                  aria-label={labels.back}\n                  className={editorBackClasses}\n                  onClick={() => {\n                    discardIncomplete();\n                    setDraft(null);\n                  }}\n                >\n                  <BackIcon />\n                </button>\n                <span className=\"min-w-0 truncate font-medium\">\n                  {field.label}\n                </span>\n                <span className=\"text-muted-foreground shrink-0\">\n                  {labels.operator(operator, condition?.value)}\n                </span>\n              </div>\n              <FilterValueEditor\n                field={field}\n                operator={operator}\n                value={condition?.value}\n                counts={getFacets?.(field.key, {\n                  conditionId: draft.conditionId,\n                  parentId,\n                  operator: operator.id,\n                })}\n                countsLoading={\n                  getFacetStatus?.(field.key, {\n                    conditionId: draft.conditionId,\n                    parentId,\n                    operator: operator.id,\n                  }) === \"loading\"\n                }\n                labels={labels}\n                locale={locale}\n                weekStartsOn={evaluateOptions.weekStartsOn}\n                onValueChange={(value) => {\n                  if (condition) {\n                    state.updateCondition(\n                      condition.id,\n                      { value },\n                      { coalesceKey },\n                    );\n                    return;\n                  }\n\n                  if (value === undefined) {\n                    return;\n                  }\n\n                  state.addNode(\n                    createFilterCondition({\n                      id: draft.conditionId,\n                      field: field.key,\n                      operator: operator.id,\n                      value,\n                    }),\n                    { coalesceKey, parentId },\n                  );\n                }}\n                onCommit={close}\n                onClose={close}\n              />\n            </div>\n          ) : (\n            <FilterFieldPicker\n              fields={fields}\n              searchLabel={labels.searchFields}\n              listLabel={labels.fieldList}\n              emptyLabel={labels.noResults}\n              onClose={close}\n              onSelect={(fieldKey) => {\n                const nextField = getFilterField(fields, fieldKey);\n                const nextOperator = nextField\n                  ? getDefaultFilterOperator(nextField)\n                  : undefined;\n\n                if (!nextField || !nextOperator) {\n                  return;\n                }\n\n                sessionRef.current += 1;\n\n                if (nextOperator.arity === \"none\") {\n                  state.addNode(\n                    createFilterCondition({\n                      field: nextField.key,\n                      operator: nextOperator.id,\n                    }),\n                    { parentId },\n                  );\n                  setAddMenuOpen(false);\n                  return;\n                }\n\n                setDraft({\n                  conditionId: createFilterId(\"condition\"),\n                  fieldKey: nextField.key,\n                  session: sessionRef.current,\n                });\n              }}\n            />\n          )}\n        </PopoverContent>\n      </Popover>\n    </>\n  );\n}\n"
    },
    {
      "path": "packages/components/src/components/filter-builder/filter-group-actions.tsx",
      "type": "registry:file",
      "target": "~/components/dethink/components/filter-builder/filter-group-actions.tsx",
      "content": "import { createContext, useContext } from \"react\";\nimport {\n  canWrapFilterNode,\n  describeFilter,\n  describeFilterCondition,\n  findFilterParent,\n} from \"./filter-core\";\nimport type { FilterNode } from \"./filter-types\";\nimport {\n  CloseIcon,\n  getNeighborItem,\n  useFilterBarContext,\n} from \"./filter-bar-parts\";\n\nexport const groupEditorClasses =\n  \"grid min-w-0 gap-[var(--dt-space-2)] text-sm\";\n\nexport const groupEditorPopoverClasses =\n  \"w-[min(36rem,calc(100vw-2rem))] max-w-none p-[var(--dt-space-3)]\";\n\nexport const groupClasses =\n  \"grid min-w-0 gap-[var(--dt-space-2)] rounded-md border border-border p-[var(--dt-space-2)] data-[top]:border-0 data-[top]:p-0 data-[negated]:border-dashed\";\n\nexport const groupHeaderClasses =\n  \"flex min-w-0 flex-wrap items-center gap-[var(--dt-space-1-5)] text-muted-foreground\";\n\nexport const groupListClasses =\n  \"grid min-w-0 gap-[var(--dt-space-1-5)] border-s border-border ps-[var(--dt-space-3)]\";\n\nexport const rowClasses =\n  \"flex min-w-0 flex-wrap items-center gap-[var(--dt-space-1)]\";\n\nexport const selectClasses =\n  \"h-7 rounded-sm border border-input bg-background px-[var(--dt-space-1-5)] text-sm text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring\";\n\nexport const toggleClasses =\n  \"inline-flex h-7 items-center rounded-sm border border-border px-[var(--dt-space-2)] text-xs font-semibold uppercase text-muted-foreground outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring aria-pressed:border-foreground aria-pressed:bg-muted aria-pressed:text-foreground\";\n\nexport const iconButtonClasses =\n  \"inline-flex size-7 shrink-0 items-center justify-center rounded-sm text-muted-foreground outline-none hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-40\";\n\nexport const groupSummaryClasses =\n  \"min-w-0 max-w-[24rem] text-muted-foreground hover:text-foreground\";\n\nexport function ArrowIcon({ direction }: { direction: \"up\" | \"down\" }) {\n  return (\n    <svg aria-hidden=\"true\" viewBox=\"0 0 16 16\" className=\"size-4\">\n      <path\n        d={\n          direction === \"up\"\n            ? \"M8 12.5v-9M4 7.5l4-4 4 4\"\n            : \"M8 3.5v9M4 8.5l4 4 4-4\"\n        }\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        strokeWidth=\"1.5\"\n      />\n    </svg>\n  );\n}\n\nexport function BracketIcon() {\n  return (\n    <svg aria-hidden=\"true\" viewBox=\"0 0 16 16\" className=\"size-4\">\n      <path\n        d=\"M5.5 2.5h-2v11h2M10.5 2.5h2v11h-2\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        strokeWidth=\"1.5\"\n      />\n    </svg>\n  );\n}\n\nexport function UngroupIcon() {\n  return (\n    <svg aria-hidden=\"true\" viewBox=\"0 0 16 16\" className=\"size-4\">\n      <path\n        d=\"M5.5 2.5h-2v4M3.5 9.5v4h2M10.5 2.5h2v4M12.5 9.5v4h-2\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        strokeWidth=\"1.5\"\n      />\n    </svg>\n  );\n}\n\nexport function TreeIcon() {\n  return (\n    <svg aria-hidden=\"true\" viewBox=\"0 0 16 16\" className=\"size-4 shrink-0\">\n      <path\n        d=\"M3 3.5h4M5 3.5v8.5h3M5 8h3M10 8h3M10 12h3\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        strokeWidth=\"1.5\"\n      />\n    </svg>\n  );\n}\n\nexport interface EditorContextValue {\n  /** Focus this element after the next render (moves re-order the DOM). */\n  focusAfterRender: (element: HTMLElement | null | undefined) => void;\n  /** Focus the add button of this group after it renders. */\n  /**\n   * Focus the first element matching `selector` inside a node's row after it\n   * renders. Used when a command remounts the row (wrap, new group).\n   */\n  focusNodeAfterRender: (nodeId: string, selector: string) => void;\n  registerNode: (nodeId: string, element: HTMLElement | null) => void;\n  topGroupId: string;\n}\n\nexport const EditorContext = createContext<EditorContextValue | null>(null);\n\nexport function useEditorContext() {\n  const context = useContext(EditorContext);\n\n  if (!context) {\n    throw new Error(\"Group editor parts must render inside FilterGroupEditor.\");\n  }\n\n  return context;\n}\n\nexport function NodeActions({ node }: { node: FilterNode }) {\n  const { describeOptions, fields, labels, maxDepth, state } =\n    useFilterBarContext(\"FilterGroupEditor\");\n  // Every row has the same actions, so names say which row they act on.\n  const target =\n    node.type === \"condition\"\n      ? describeFilterCondition(node, fields, describeOptions)\n      : describeFilter(node, fields, describeOptions);\n  const name = (action: string) => `${action}, ${target}`;\n  const { focusAfterRender, focusNodeAfterRender, topGroupId } =\n    useEditorContext();\n  const location = findFilterParent(state.filter, node.id);\n  const index = location?.index ?? 0;\n  const last = (location?.parent.children.length ?? 1) - 1;\n  const canWrap = canWrapFilterNode(state.filter, node.id, maxDepth);\n\n  const shift = (offset: -1 | 1) => {\n    const next = index + offset;\n    // At an edge the pressed arrow becomes disabled, so focus the other one.\n    const item =\n      next === 0 ? \"move-down\" : next === last ? \"move-up\" : undefined;\n\n    focusNodeAfterRender(\n      node.id,\n      `[data-filter-bar-item=\"${item ?? (offset < 0 ? \"move-up\" : \"move-down\")}\"]`,\n    );\n    state.shiftNode(node.id, offset);\n  };\n\n  return (\n    <span data-slot=\"filter-node-actions\" className=\"inline-flex items-center\">\n      <button\n        type=\"button\"\n        data-filter-bar-item=\"negate\"\n        aria-label={name(labels.negate)}\n        aria-pressed={Boolean(node.not)}\n        className={toggleClasses}\n        onClick={() => {\n          state.setNegated(node.id, !node.not);\n        }}\n      >\n        {labels.negate}\n      </button>\n      <button\n        type=\"button\"\n        data-filter-bar-item=\"move-up\"\n        aria-label={name(labels.moveUp)}\n        title={`${labels.moveUp} (Alt+↑)`}\n        disabled={index === 0}\n        className={iconButtonClasses}\n        onClick={() => {\n          shift(-1);\n        }}\n      >\n        <ArrowIcon direction=\"up\" />\n      </button>\n      <button\n        type=\"button\"\n        data-filter-bar-item=\"move-down\"\n        aria-label={name(labels.moveDown)}\n        title={`${labels.moveDown} (Alt+↓)`}\n        disabled={index === last}\n        className={iconButtonClasses}\n        onClick={() => {\n          shift(1);\n        }}\n      >\n        <ArrowIcon direction=\"down\" />\n      </button>\n      <button\n        type=\"button\"\n        data-filter-bar-item=\"wrap\"\n        aria-label={name(labels.wrapInGroup)}\n        title={canWrap ? labels.wrapInGroup : labels.maxDepthReached(maxDepth)}\n        disabled={!canWrap}\n        className={iconButtonClasses}\n        onClick={() => {\n          focusNodeAfterRender(node.id, '[data-filter-bar-item=\"wrap\"]');\n          state.wrapInGroup(node.id);\n        }}\n      >\n        <BracketIcon />\n      </button>\n      {node.type === \"group\" && node.id !== topGroupId ? (\n        <>\n          <button\n            type=\"button\"\n            data-filter-bar-item=\"ungroup\"\n            aria-label={name(labels.ungroup)}\n            title={labels.ungroup}\n            className={iconButtonClasses}\n            onClick={(event) => {\n              const row = event.currentTarget.closest(\"li\");\n\n              focusAfterRender(getNeighborItem(row));\n              state.unwrapGroup(node.id);\n            }}\n          >\n            <UngroupIcon />\n          </button>\n          <button\n            type=\"button\"\n            data-filter-bar-item=\"remove\"\n            aria-label={name(labels.removeGroup)}\n            title={labels.removeGroup}\n            className={iconButtonClasses}\n            onClick={(event) => {\n              const row = event.currentTarget.closest(\"li\");\n\n              focusAfterRender(getNeighborItem(row));\n              state.removeNode(node.id);\n            }}\n          >\n            <CloseIcon />\n          </button>\n        </>\n      ) : null}\n    </span>\n  );\n}\n"
    },
    {
      "path": "packages/components/src/components/filter-builder/filter-group-editor.tsx",
      "type": "registry:file",
      "target": "~/components/dethink/components/filter-builder/filter-group-editor.tsx",
      "content": "import {\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  type HTMLAttributes,\n  type KeyboardEvent,\n} from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport {\n  canAddFilterGroup,\n  createFilter,\n  describeFilter,\n  findFilterNode,\n  getFilterNodeDepth,\n} from \"./filter-core\";\nimport {\n  groupEditorClasses,\n  groupClasses,\n  groupHeaderClasses,\n  groupListClasses,\n  rowClasses,\n  selectClasses,\n  toggleClasses,\n  type EditorContextValue,\n  EditorContext,\n  useEditorContext,\n  NodeActions,\n} from \"./filter-group-actions\";\nimport { FilterAddMenu } from \"./filter-add-menu\";\nimport { FilterChip } from \"./filter-chip\";\nimport type { FilterCombinator, FilterGroup, FilterNode } from \"./filter-types\";\nimport {\n  PlusIcon,\n  filterBarActionClassNames,\n  useFilterBarContext,\n} from \"./filter-bar-parts\";\n\nfunction GroupNode({ group }: { group: FilterGroup }) {\n  const { describeOptions, fields, labels, maxDepth, state } =\n    useFilterBarContext(\"FilterGroupEditor\");\n  const { focusAfterRender, focusNodeAfterRender, registerNode, topGroupId } =\n    useEditorContext();\n  const headingId = useId();\n  const depth = getFilterNodeDepth(state.filter, group.id) ?? 1;\n  const isTop = group.id === topGroupId;\n  const canNest = canAddFilterGroup(state.filter, group.id, maxDepth);\n  const summary =\n    group.children.length > 0\n      ? describeFilter(group, fields, describeOptions)\n      : labels.emptyGroup;\n\n  const handleRowKeyDown = (\n    event: KeyboardEvent<HTMLLIElement>,\n    node: FilterNode,\n  ) => {\n    if (\n      !event.altKey ||\n      (event.key !== \"ArrowUp\" && event.key !== \"ArrowDown\") ||\n      !(event.target instanceof HTMLElement) ||\n      event.target.closest(\"li\") !== event.currentTarget\n    ) {\n      return;\n    }\n\n    event.preventDefault();\n    event.stopPropagation();\n    focusAfterRender(event.target);\n    state.shiftNode(node.id, event.key === \"ArrowUp\" ? -1 : 1);\n  };\n\n  return (\n    <div\n      ref={(element) => {\n        registerNode(group.id, element);\n      }}\n      role=\"group\"\n      aria-labelledby={headingId}\n      data-slot=\"filter-group\"\n      data-depth={depth}\n      data-top={isTop ? \"\" : undefined}\n      data-negated={group.not ? \"\" : undefined}\n      className={groupClasses}\n    >\n      <div data-slot=\"filter-group-header\" className={groupHeaderClasses}>\n        <span id={headingId} className=\"sr-only\">\n          {isTop && depth === 1\n            ? labels.groupEditor\n            : labels.editGroup(summary)}\n        </span>\n        <span aria-hidden=\"true\">{labels.matchPrefix}</span>\n        <select\n          aria-label={`${labels.matchPrefix} ${labels.matchSuffix}`}\n          data-slot=\"filter-group-combinator\"\n          value={group.combinator}\n          className={selectClasses}\n          onChange={(event) => {\n            state.setCombinator(\n              group.id,\n              event.currentTarget.value as FilterCombinator,\n            );\n          }}\n        >\n          <option value=\"and\">{labels.matchAll}</option>\n          <option value=\"or\">{labels.matchAny}</option>\n        </select>\n        <span aria-hidden=\"true\">{labels.matchSuffix}</span>\n        {isTop ? (\n          <button\n            type=\"button\"\n            data-filter-bar-item=\"negate\"\n            aria-pressed={Boolean(group.not)}\n            className={toggleClasses}\n            onClick={() => {\n              state.setNegated(group.id, !group.not);\n            }}\n          >\n            {labels.negate}\n          </button>\n        ) : (\n          <NodeActions node={group} />\n        )}\n      </div>\n      {group.children.length > 0 ? (\n        <ul className={groupListClasses}>\n          {group.children.map((child) => (\n            // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- Alt+Arrow moves the row that holds the focused control.\n            <li\n              key={child.id}\n              data-slot=\"filter-group-row\"\n              className=\"min-w-0\"\n              onKeyDown={(event) => {\n                handleRowKeyDown(event, child);\n              }}\n            >\n              {child.type === \"condition\" ? (\n                <div\n                  ref={(element) => {\n                    registerNode(child.id, element);\n                  }}\n                  className={rowClasses}\n                >\n                  <FilterChip condition={child} />\n                  <NodeActions node={child} />\n                </div>\n              ) : (\n                <GroupNode group={child} />\n              )}\n            </li>\n          ))}\n        </ul>\n      ) : null}\n      <div className={rowClasses}>\n        <FilterAddMenu parentId={group.id} className=\"h-7 text-xs\">\n          <PlusIcon />\n          {labels.addCondition}\n        </FilterAddMenu>\n        <button\n          type=\"button\"\n          data-filter-bar-item=\"add-group\"\n          disabled={!canNest}\n          title={canNest ? undefined : labels.maxDepthReached(maxDepth)}\n          className={filterBarActionClassNames({\n            className:\n              \"h-7 text-xs disabled:pointer-events-none disabled:opacity-40\",\n          })}\n          onClick={() => {\n            const next = createFilter();\n\n            focusNodeAfterRender(\n              next.id,\n              '[data-slot=\"filter-add-menu-trigger\"]',\n            );\n            state.addNode(next, { parentId: group.id });\n          }}\n        >\n          <PlusIcon />\n          {labels.addGroup}\n        </button>\n      </div>\n    </div>\n  );\n}\n\nexport interface FilterGroupEditorProps extends Omit<\n  HTMLAttributes<HTMLDivElement>,\n  \"children\"\n> {\n  /** Group to edit. Defaults to the root of the filter. */\n  groupId?: string;\n}\n\n/**\n * Nested AND/OR editor for a filter group, rendered inside a FilterBar.\n * Tab moves through the controls, Alt+ArrowUp/ArrowDown moves the focused\n * row, and Cmd/Ctrl+Z undoes.\n */\nexport function FilterGroupEditor({\n  className,\n  groupId,\n  onKeyDown,\n  ...props\n}: FilterGroupEditorProps) {\n  const { state } = useFilterBarContext(\"FilterGroupEditor\");\n  const nodeElements = useRef(new Map<string, HTMLElement>());\n  const pendingElement = useRef<HTMLElement | null>(null);\n  const pendingNode = useRef<{ id: string; selector: string } | null>(null);\n  const node = groupId ? findFilterNode(state.filter, groupId) : state.filter;\n  const topGroupId = node?.id ?? state.filter.id;\n\n  useEffect(() => {\n    const element = pendingElement.current;\n    const target = pendingNode.current;\n\n    pendingElement.current = null;\n    pendingNode.current = null;\n\n    const row = target ? nodeElements.current.get(target.id) : undefined;\n    const preferred = target\n      ? row?.querySelector<HTMLButtonElement>(target.selector)\n      : undefined;\n    // A command can leave the preferred control disabled (e.g. wrapping at\n    // the depth limit), so fall back to the row's first usable control.\n    const nodeTarget =\n      preferred && !preferred.disabled\n        ? preferred\n        : row?.querySelector<HTMLElement>(\n            \"[data-filter-bar-item]:not(:disabled)\",\n          );\n\n    if (nodeTarget) {\n      nodeTarget.focus();\n    } else if (element?.isConnected && document.activeElement !== element) {\n      element.focus();\n    }\n  });\n\n  const contextValue = useMemo<EditorContextValue>(\n    () => ({\n      focusAfterRender: (element) => {\n        pendingElement.current = element ?? null;\n      },\n      focusNodeAfterRender: (id, selector) => {\n        pendingNode.current = { id, selector };\n      },\n      registerNode: (id, element) => {\n        if (element) {\n          nodeElements.current.set(id, element);\n        } else {\n          nodeElements.current.delete(id);\n        }\n      },\n      topGroupId,\n    }),\n    [topGroupId],\n  );\n\n  if (node?.type !== \"group\") {\n    return null;\n  }\n\n  return (\n    <EditorContext.Provider value={contextValue}>\n      {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- Handles undo for the editor's own controls. */}\n      <div\n        {...props}\n        data-slot=\"filter-group-editor\"\n        data-filter-focus-scope=\"\"\n        className={cn(groupEditorClasses, className)}\n        onKeyDown={(event) => {\n          onKeyDown?.(event);\n\n          const target = event.target;\n\n          if (\n            event.defaultPrevented ||\n            !(event.metaKey || event.ctrlKey) ||\n            event.key.toLowerCase() !== \"z\" ||\n            !(target instanceof HTMLElement) ||\n            target.tagName === \"INPUT\" ||\n            target.tagName === \"TEXTAREA\"\n          ) {\n            return;\n          }\n\n          event.preventDefault();\n          // Keep the bar's toolbar from undoing a second time.\n          event.stopPropagation();\n\n          if (event.shiftKey) {\n            state.redo();\n          } else {\n            state.undo();\n          }\n        }}\n      >\n        <GroupNode group={node} />\n      </div>\n    </EditorContext.Provider>\n  );\n}\n"
    },
    {
      "path": "packages/components/src/components/filter-builder/filter-group-chip.tsx",
      "type": "registry:file",
      "target": "~/components/dethink/components/filter-builder/filter-group-chip.tsx",
      "content": "import { useRef, useState, type HTMLAttributes, type ReactNode } from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { Popover, PopoverContent } from \"../popover\";\nimport { describeFilter } from \"./filter-core\";\nimport {\n  groupEditorPopoverClasses,\n  groupSummaryClasses,\n  TreeIcon,\n} from \"./filter-group-actions\";\nimport { FilterGroupEditor } from \"./filter-group-editor\";\nimport type { FilterGroup } from \"./filter-types\";\nimport {\n  CloseIcon,\n  filterBarActionClassNames,\n  filterChipClassNames,\n  getNeighborItem,\n  isRemoveKey,\n  segmentClasses,\n  segmentRemoveClasses,\n  useEditorFocusReturn,\n  useFilterBarContext,\n} from \"./filter-bar-parts\";\n\nexport interface FilterGroupChipProps extends Omit<\n  HTMLAttributes<HTMLDivElement>,\n  \"children\"\n> {\n  group: FilterGroup;\n  index?: number;\n}\n\n/** Summary of a nested group. Opens the group editor; × removes the group. */\nexport function FilterGroupChip({\n  className,\n  group,\n  index = 0,\n  ...props\n}: FilterGroupChipProps) {\n  const {\n    collapseAfter,\n    describeOptions,\n    expanded,\n    fields,\n    labels,\n    requestFocus,\n    size,\n    state,\n  } = useFilterBarContext(\"FilterGroupChip\");\n  const [open, setOpen] = useState(false);\n  const chipRef = useRef<HTMLDivElement>(null);\n  const returnFocusRef = useEditorFocusReturn(open);\n  const description =\n    group.children.length > 0\n      ? describeFilter(group, fields, describeOptions)\n      : labels.emptyGroup;\n\n  const remove = () => {\n    requestFocus(getNeighborItem(chipRef.current));\n    state.removeNode(group.id);\n  };\n\n  return (\n    // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- Keys delegate to the chip's roving segment buttons.\n    <div\n      {...props}\n      ref={chipRef}\n      role=\"group\"\n      aria-label={description}\n      data-slot=\"filter-group-chip\"\n      data-negated={group.not ? \"\" : undefined}\n      data-overflow={index >= collapseAfter && !expanded ? \"\" : undefined}\n      className={filterChipClassNames({ size, className })}\n      onKeyDown={(event) => {\n        if (isRemoveKey(event)) {\n          event.preventDefault();\n          remove();\n        }\n      }}\n    >\n      <button\n        type=\"button\"\n        data-filter-bar-item=\"group\"\n        data-slot=\"filter-group-chip-trigger\"\n        aria-label={labels.editGroup(description)}\n        aria-haspopup=\"dialog\"\n        aria-expanded={open}\n        className={cn(segmentClasses, groupSummaryClasses)}\n        onClick={(event) => {\n          returnFocusRef.current = event.currentTarget;\n          setOpen(true);\n        }}\n      >\n        <span className=\"min-w-0 truncate\">{description}</span>\n      </button>\n      <button\n        type=\"button\"\n        data-filter-bar-item=\"remove\"\n        data-slot=\"filter-chip-remove\"\n        aria-label={labels.remove(description)}\n        className={cn(segmentClasses, segmentRemoveClasses)}\n        onClick={remove}\n      >\n        <CloseIcon />\n      </button>\n      <Popover anchorRef={chipRef} open={open} onOpenChange={setOpen}>\n        <PopoverContent\n          aria-label={labels.editGroup(description)}\n          placement=\"bottom start\"\n          className={groupEditorPopoverClasses}\n        >\n          <FilterGroupEditor groupId={group.id} />\n        </PopoverContent>\n      </Popover>\n    </div>\n  );\n}\n\nexport interface FilterBarAdvancedProps {\n  className?: string;\n  children?: ReactNode;\n}\n\n/** Opens the group editor for the whole filter. Hidden while it is empty. */\nexport function FilterBarAdvanced({\n  children,\n  className,\n}: FilterBarAdvancedProps) {\n  const { labels, size, state } = useFilterBarContext(\"FilterBarAdvanced\");\n  const [open, setOpen] = useState(false);\n  const buttonRef = useRef<HTMLButtonElement>(null);\n\n  if (state.filter.children.length === 0) {\n    return null;\n  }\n\n  return (\n    <>\n      <button\n        ref={buttonRef}\n        type=\"button\"\n        data-filter-bar-item=\"advanced\"\n        data-slot=\"filter-bar-advanced\"\n        aria-haspopup=\"dialog\"\n        aria-expanded={open}\n        className={filterBarActionClassNames({ size, className })}\n        onClick={() => {\n          setOpen(!open);\n        }}\n      >\n        {children ?? (\n          <>\n            <TreeIcon />\n            {labels.advanced}\n          </>\n        )}\n      </button>\n      <Popover anchorRef={buttonRef} open={open} onOpenChange={setOpen}>\n        <PopoverContent\n          aria-label={labels.groupEditor}\n          placement=\"bottom start\"\n          className={groupEditorPopoverClasses}\n        >\n          <FilterGroupEditor />\n        </PopoverContent>\n      </Popover>\n    </>\n  );\n}\n"
    },
    {
      "path": "packages/components/src/components/filter-builder/filter-bar-rescue.tsx",
      "type": "registry:file",
      "target": "~/components/dethink/components/filter-builder/filter-bar-rescue.tsx",
      "content": "import { describeFilterCondition } from \"./filter-core\";\nimport {\n  filterBarActionClassNames,\n  useFilterBarContext,\n} from \"./filter-bar-parts\";\n\nexport interface FilterBarRescueProps {\n  className?: string;\n}\n\n/**\n * Shown when no rows match: names the most restrictive condition and offers\n * to remove it (\"Relax\") as one undoable step. Needs FilterBar `data`.\n */\nexport function FilterBarRescue({ className }: FilterBarRescueProps) {\n  const { describeOptions, fields, labels, rescue, size, state } =\n    useFilterBarContext(\"FilterBarRescue\");\n\n  if (!rescue) {\n    return null;\n  }\n\n  const description = describeFilterCondition(\n    rescue.condition,\n    fields,\n    describeOptions,\n  );\n\n  return (\n    <div\n      data-slot=\"filter-bar-rescue\"\n      className=\"text-muted-foreground flex min-w-0 flex-wrap items-center gap-[var(--dt-space-2)] text-sm\"\n    >\n      <span className=\"min-w-0\">\n        {labels.rescue(description, rescue.count)}\n      </span>\n      <button\n        type=\"button\"\n        data-slot=\"filter-bar-relax\"\n        aria-label={`${labels.relax}: ${description}`}\n        className={filterBarActionClassNames({\n          size,\n          className: `border-border text-foreground border ${className ?? \"\"}`,\n        })}\n        onClick={() => {\n          state.removeNode(rescue.condition.id);\n        }}\n      >\n        {labels.relax}\n      </button>\n    </div>\n  );\n}\n"
    },
    {
      "path": "packages/components/src/components/filter-builder/filter-bar-chips.tsx",
      "type": "registry:file",
      "target": "~/components/dethink/components/filter-builder/filter-bar-chips.tsx",
      "content": "import { type ReactNode } from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport type { FilterCondition } from \"./filter-types\";\nimport { FilterChip } from \"./filter-chip\";\nimport { FilterGroupChip } from \"./filter-group-chip\";\nimport {\n  UndoIcon,\n  conjunctionClasses,\n  filterBarActionClassNames,\n  moreButtonClasses,\n  negationClasses,\n  useFilterBarContext,\n} from \"./filter-bar-parts\";\n\nexport interface FilterBarChipsProps {\n  /** Custom chip renderer. Return `undefined` to use the default chip. */\n  renderChip?: (condition: FilterCondition, index: number) => ReactNode;\n}\n\n/** Chips for the root conditions, with narrow-width collapsing. */\nexport function FilterBarChips({ renderChip }: FilterBarChipsProps) {\n  const { collapseAfter, expanded, labels, setExpanded, size, state } =\n    useFilterBarContext(\"FilterBarChips\");\n  const { children, combinator } = state.filter;\n  const hiddenCount = Math.max(children.length - collapseAfter, 0);\n\n  return (\n    <>\n      {state.filter.not && children.length > 0 ? (\n        <span data-slot=\"filter-bar-not\" className={negationClasses}>\n          {labels.notMatching}\n        </span>\n      ) : null}\n      {children.map((node, index) => (\n        <FilterBarChipSlot\n          key={node.id}\n          conjunction={index > 0 && combinator === \"or\" ? labels.or : undefined}\n        >\n          {node.type === \"condition\" ? (\n            (renderChip?.(node, index) ?? (\n              <FilterChip condition={node} index={index} />\n            ))\n          ) : (\n            <FilterGroupChip group={node} index={index} />\n          )}\n        </FilterBarChipSlot>\n      ))}\n      {hiddenCount > 0 ? (\n        <button\n          type=\"button\"\n          data-filter-bar-item=\"more\"\n          data-slot=\"filter-bar-more\"\n          aria-expanded={expanded}\n          className={filterBarActionClassNames({\n            size,\n            className: moreButtonClasses,\n          })}\n          onClick={() => {\n            setExpanded(!expanded);\n          }}\n        >\n          {expanded ? labels.showLess : labels.showMore(hiddenCount)}\n        </button>\n      ) : null}\n    </>\n  );\n}\n\nfunction FilterBarChipSlot({\n  children,\n  conjunction,\n}: {\n  children: ReactNode;\n  conjunction?: string;\n}) {\n  if (!conjunction) {\n    return children;\n  }\n\n  return (\n    <>\n      <span aria-hidden=\"true\" className={conjunctionClasses}>\n        {conjunction}\n      </span>\n      {children}\n    </>\n  );\n}\n\nexport interface FilterBarActionProps {\n  className?: string;\n  children?: ReactNode;\n}\n\n/** Removes every filter. Hidden while the filter is empty. */\nexport function FilterBarClear({ children, className }: FilterBarActionProps) {\n  const { addButtonRef, labels, requestFocus, size, state } =\n    useFilterBarContext(\"FilterBarClear\");\n\n  if (state.filter.children.length === 0) {\n    return null;\n  }\n\n  return (\n    <button\n      type=\"button\"\n      data-filter-bar-item=\"clear\"\n      data-slot=\"filter-bar-clear\"\n      className={filterBarActionClassNames({ size, className })}\n      onClick={() => {\n        requestFocus(addButtonRef.current);\n        state.clear();\n      }}\n    >\n      {children ?? labels.clear}\n    </button>\n  );\n}\n\n/** Undoes the last filter change. Hidden when there is nothing to undo. */\nexport function FilterBarUndo({ children, className }: FilterBarActionProps) {\n  const { labels, size, state } = useFilterBarContext(\"FilterBarUndo\");\n\n  if (!state.canUndo) {\n    return null;\n  }\n\n  return (\n    <button\n      type=\"button\"\n      data-filter-bar-item=\"undo\"\n      data-slot=\"filter-bar-undo\"\n      aria-label={children ? undefined : labels.undo}\n      title={children ? undefined : labels.undo}\n      className={filterBarActionClassNames({\n        size,\n        className: cn(\n          children ? undefined : \"px-[var(--dt-space-1-5)]\",\n          className,\n        ),\n      })}\n      onClick={() => {\n        state.undo();\n      }}\n    >\n      {children ?? <UndoIcon />}\n    </button>\n  );\n}\n"
    },
    {
      "path": "packages/components/src/components/filter-builder/filter-bar.tsx",
      "type": "registry:file",
      "target": "~/components/dethink/components/filter-builder/filter-bar.tsx",
      "content": "import {\n  useCallback,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n  type HTMLAttributes,\n  type KeyboardEvent,\n  type ReactNode,\n} from \"react\";\nimport { LiveRegion } from \"../live-region\";\nimport {\n  DEFAULT_FILTER_MAX_DEPTH,\n  describeFilter,\n  describeFilterCondition,\n  type DescribeFilterOptions,\n} from \"./filter-core\";\nimport type {\n  Filter,\n  FilterEvaluateOptions,\n  FilterFields,\n} from \"./filter-types\";\nimport { FilterBarRescue } from \"./filter-bar-rescue\";\nimport { useFilterInsights } from \"./use-filter-insights\";\nimport { useServerFacets, type GetFilterFacets } from \"./use-server-facets\";\nimport { useFilterState, type FilterState } from \"./use-filter-state\";\nimport { FilterAddMenu } from \"./filter-add-menu\";\nimport { FilterBarAdvanced } from \"./filter-group-chip\";\nimport {\n  FilterBarContext,\n  ITEM_SELECTOR,\n  defaultFilterBarLabels,\n  filterBarClassNames,\n  filterBarToolbarClasses,\n  isEditableTarget,\n  isVisible,\n  type FilterBarContextValue,\n  type FilterBarLabels,\n  type FilterBarSize,\n} from \"./filter-bar-parts\";\n\nimport {\n  FilterBarChips,\n  FilterBarClear,\n  FilterBarUndo,\n} from \"./filter-bar-chips\";\n\nexport interface FilterBarProps<TData = unknown> extends Omit<\n  HTMLAttributes<HTMLDivElement>,\n  \"children\" | \"defaultValue\" | \"onChange\"\n> {\n  fields: FilterFields<TData>;\n  value?: Filter;\n  defaultValue?: Filter;\n  onValueChange?: (filter: Filter) => void;\n  /** External state from `useFilterState`. Takes precedence over value props. */\n  state?: FilterState;\n  labels?: Partial<FilterBarLabels>;\n  /** Number of matching rows, announced politely when it changes. */\n  resultCount?: number;\n  /** Single key that opens the add menu when focus is not in a text field. */\n  addShortcut?: string | false;\n  /** Chips shown before \"+N more\" on narrow containers. Defaults to 2. */\n  collapseAfter?: number;\n  /** Group levels allowed, counting the root as 1. Defaults to 3. */\n  maxDepth?: number;\n  /**\n   * Rows to filter on the client. Enables facet counts in option pickers,\n   * an announced result count and the empty-result rescue.\n   */\n  data?: readonly TData[];\n  /**\n   * Facet counts from your server, for server-side data: called with the\n   * field and the filter to count under. Takes precedence over counts from\n   * `data`. Pickers reserve space while counts load.\n   */\n  getFacets?: GetFilterFacets;\n  /**\n   * Cache key for server facets besides the filter, e.g. a time range or a\n   * data version: counts are fetched again when it changes. Compared as\n   * JSON, so use a primitive or plain JSON value. `getFacets` itself may be\n   * an inline function.\n   */\n  facetsKey?: unknown;\n  /** Shows how many rows each chip removes (\"−42\"). Needs `data`. */\n  showImpact?: boolean;\n  /** Offers \"Relax\" on the most restrictive chip when nothing matches. */\n  rescue?: boolean;\n  /**\n   * `now`, `timeZone` and `weekStartsOn` for relative dates. Inject `now`\n   * so server and client counts match.\n   */\n  evaluateOptions?: FilterEvaluateOptions;\n  /** Locale for numbers, dates and calendars. Defaults to \"en-US\". */\n  locale?: string;\n  size?: FilterBarSize;\n  /** Custom composition of FilterBar parts. Defaults to chips and actions. */\n  children?: ReactNode;\n}\n\n/**\n * Filter chips in a keyboard-navigable toolbar. Arrow keys move between\n * segments, Backspace/Delete removes the focused chip and Cmd/Ctrl+Z undoes.\n */\nexport function FilterBar<TData>({\n  \"aria-label\": ariaLabel,\n  addShortcut = false,\n  children,\n  className,\n  collapseAfter = 2,\n  data,\n  defaultValue,\n  evaluateOptions: evaluateOptionsProp,\n  fields,\n  facetsKey,\n  getFacets: getServerFacets,\n  labels: labelOverrides,\n  locale = \"en-US\",\n  maxDepth = DEFAULT_FILTER_MAX_DEPTH,\n  onValueChange,\n  rescue: withRescue = true,\n  resultCount,\n  showImpact = false,\n  size = \"md\",\n  state: externalState,\n  value,\n  ...props\n}: FilterBarProps<TData>) {\n  const internalState = useFilterState({ defaultValue, onValueChange, value });\n  const state = externalState ?? internalState;\n  const labels = useMemo(\n    () => ({ ...defaultFilterBarLabels, ...labelOverrides }),\n    [labelOverrides],\n  );\n  const describeOptions = useMemo<DescribeFilterOptions>(\n    () => ({\n      locale,\n      labels: {\n        and: labels.and,\n        empty: labels.empty,\n        missingValue: labels.missingValue,\n        not: labels.not,\n        or: labels.or,\n      },\n    }),\n    [labels, locale],\n  );\n  // A fixed default `now` keeps relative dates and counts stable across\n  // renders; pass `evaluateOptions.now` to match server output.\n  const [mountedAt] = useState(() => Date.now());\n  const evaluateNow = evaluateOptionsProp?.now ?? mountedAt;\n  const evaluateTimeZone = evaluateOptionsProp?.timeZone;\n  const evaluateWeekStart = evaluateOptionsProp?.weekStartsOn ?? 1;\n  const evaluateOptions = useMemo<FilterEvaluateOptions>(\n    () => ({\n      now: evaluateNow,\n      timeZone: evaluateTimeZone,\n      weekStartsOn: evaluateWeekStart,\n    }),\n    [evaluateNow, evaluateTimeZone, evaluateWeekStart],\n  );\n  const insights = useFilterInsights({\n    data,\n    evaluateOptions,\n    fields,\n    filter: state.filter,\n    impact: showImpact,\n    rescue: withRescue,\n  });\n  const serverFacets = useServerFacets({\n    facetsKey,\n    fields,\n    filter: state.filter,\n    getFacets: getServerFacets,\n  });\n  const [addMenuOpen, setAddMenuOpen] = useState(false);\n  const [expanded, setExpanded] = useState(false);\n  const toolbarRef = useRef<HTMLDivElement>(null);\n  const addButtonRef = useRef<HTMLButtonElement>(null);\n  const activeItemRef = useRef<HTMLElement | null>(null);\n  const pendingFocusRef = useRef<HTMLElement | null>(null);\n  // Position of the tab stop, used to recover focus if its element is removed.\n  const activeIndexRef = useRef(0);\n  // Whether focus is in the toolbar, so recovery never steals focus.\n  const focusWithinRef = useRef(false);\n  const summaryId = useId();\n\n  const getAllItems = useCallback(() => {\n    const toolbar = toolbarRef.current;\n\n    return toolbar\n      ? [...toolbar.querySelectorAll<HTMLElement>(ITEM_SELECTOR)].filter(\n          // Items inside a nested focus scope (an inline group editor) keep\n          // their own tab order.\n          (item) => item.closest(\"[data-filter-focus-scope]\") === toolbar,\n        )\n      : [];\n  }, []);\n\n  const getItems = useCallback(\n    () => getAllItems().filter(isVisible),\n    [getAllItems],\n  );\n\n  const syncTabStops = useCallback(() => {\n    const items = getAllItems();\n    const visible = items.filter(isVisible);\n    const activeItem = activeItemRef.current;\n    const tabStop =\n      activeItem && visible.includes(activeItem) ? activeItem : visible[0];\n\n    // Hidden (collapsed) items get -1 too, so revealing them later never\n    // exposes extra tab stops.\n    for (const item of items) {\n      item.tabIndex = item === tabStop ? 0 : -1;\n    }\n\n    if (tabStop) {\n      activeIndexRef.current = visible.indexOf(tabStop);\n    }\n  }, [getAllItems]);\n\n  const requestFocus = useCallback((element?: HTMLElement | null) => {\n    pendingFocusRef.current = element ?? null;\n  }, []);\n\n  // Roving tabindex: runs after every render so added/removed chips keep a\n  // single tab stop, and moves focus after a chip is removed.\n  useEffect(() => {\n    const pending = pendingFocusRef.current;\n\n    pendingFocusRef.current = null;\n\n    const lost = activeItemRef.current;\n\n    if (pending?.isConnected) {\n      activeItemRef.current = pending;\n      pending.focus();\n    } else if (lost && !lost.isConnected) {\n      activeItemRef.current = null;\n\n      // A render removed the focused item (e.g. undo hid the Undo button or\n      // a chip). Keep keyboard users in the toolbar at the same position.\n      const focusLost =\n        document.activeElement === null ||\n        document.activeElement === document.body;\n\n      if (focusWithinRef.current && focusLost) {\n        const visible = getItems();\n        const next =\n          visible[Math.min(activeIndexRef.current, visible.length - 1)];\n\n        if (next) {\n          activeItemRef.current = next;\n          next.focus();\n        }\n      }\n    }\n\n    syncTabStops();\n  });\n\n  // Chips collapse through a container query, which does not re-render, so\n  // re-sync when the toolbar's size changes.\n  useEffect(() => {\n    const toolbar = toolbarRef.current;\n\n    if (!toolbar || typeof ResizeObserver === \"undefined\") {\n      return undefined;\n    }\n\n    const observer = new ResizeObserver(() => {\n      syncTabStops();\n    });\n\n    observer.observe(toolbar);\n\n    return () => {\n      observer.disconnect();\n    };\n  }, [syncTabStops]);\n\n  useEffect(() => {\n    if (!addShortcut) {\n      return undefined;\n    }\n\n    const handleKeyDown = (event: globalThis.KeyboardEvent) => {\n      if (\n        event.defaultPrevented ||\n        event.metaKey ||\n        event.ctrlKey ||\n        event.altKey ||\n        event.key.toLowerCase() !== addShortcut.toLowerCase() ||\n        isEditableTarget(event.target)\n      ) {\n        return;\n      }\n\n      event.preventDefault();\n      addButtonRef.current?.focus();\n      setAddMenuOpen(true);\n    };\n\n    document.addEventListener(\"keydown\", handleKeyDown);\n\n    return () => {\n      document.removeEventListener(\"keydown\", handleKeyDown);\n    };\n  }, [addShortcut]);\n\n  const contextValue = useMemo<FilterBarContextValue>(\n    () => ({\n      addButtonRef,\n      addMenuOpen,\n      collapseAfter,\n      describeOptions,\n      evaluateOptions,\n      expanded,\n      fields: fields as FilterFields,\n      getFacets: serverFacets?.getFacets ?? insights.getFacets,\n      getFacetStatus: serverFacets?.getFacetStatus,\n      impact: insights.impact,\n      labels,\n      locale,\n      maxDepth,\n      rescue: insights.rescue,\n      requestFocus,\n      setAddMenuOpen,\n      setExpanded,\n      size,\n      state,\n    }),\n    [\n      addMenuOpen,\n      collapseAfter,\n      describeOptions,\n      evaluateOptions,\n      expanded,\n      fields,\n      insights,\n      labels,\n      serverFacets,\n      locale,\n      maxDepth,\n      requestFocus,\n      size,\n      state,\n    ],\n  );\n\n  const announcedCount = resultCount ?? insights.count;\n  const rescueText = insights.rescue\n    ? labels.rescue(\n        describeFilterCondition(\n          insights.rescue.condition,\n          fields,\n          describeOptions,\n        ),\n        insights.rescue.count,\n      )\n    : undefined;\n\n  const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n    const target = event.target;\n\n    if (\n      !(target instanceof HTMLElement) ||\n      !target.hasAttribute(\"data-filter-bar-item\")\n    ) {\n      return;\n    }\n\n    const modifier = event.metaKey || event.ctrlKey;\n\n    if (modifier && event.key.toLowerCase() === \"z\") {\n      event.preventDefault();\n\n      if (event.shiftKey) {\n        state.redo();\n      } else {\n        state.undo();\n      }\n\n      return;\n    }\n\n    const items = getItems();\n    const index = items.indexOf(target);\n\n    if (index === -1) {\n      return;\n    }\n\n    const rtl =\n      toolbarRef.current !== null &&\n      getComputedStyle(toolbarRef.current).direction === \"rtl\";\n    let next: HTMLElement | undefined;\n\n    switch (event.key) {\n      case \"ArrowRight\":\n        next = items[rtl ? index - 1 : index + 1];\n        break;\n      case \"ArrowLeft\":\n        next = items[rtl ? index + 1 : index - 1];\n        break;\n      case \"Home\":\n        next = items[0];\n        break;\n      case \"End\":\n        next = items.at(-1);\n        break;\n      default:\n        return;\n    }\n\n    event.preventDefault();\n\n    if (next) {\n      activeItemRef.current = next;\n      syncTabStops();\n      next.focus();\n    }\n  };\n\n  return (\n    <FilterBarContext.Provider value={contextValue}>\n      <div\n        {...props}\n        data-slot=\"filter-bar\"\n        data-size={size}\n        data-empty={state.filter.children.length === 0 ? \"\" : undefined}\n        className={filterBarClassNames({ className })}\n      >\n        <div\n          ref={toolbarRef}\n          role=\"toolbar\"\n          data-filter-focus-scope=\"\"\n          aria-label={ariaLabel ?? labels.toolbar}\n          aria-describedby={summaryId}\n          data-slot=\"filter-bar-toolbar\"\n          className={filterBarToolbarClasses}\n          onKeyDown={handleKeyDown}\n          onBlur={(event) => {\n            // A removed element blurs with no related target; only a move\n            // to somewhere else counts as leaving.\n            if (\n              event.relatedTarget instanceof Node &&\n              !event.currentTarget.contains(event.relatedTarget)\n            ) {\n              focusWithinRef.current = false;\n            }\n          }}\n          onFocus={(event) => {\n            focusWithinRef.current = true;\n\n            if (\n              event.target instanceof HTMLElement &&\n              event.target.hasAttribute(\"data-filter-bar-item\") &&\n              event.target.closest(\"[data-filter-focus-scope]\") ===\n                toolbarRef.current\n            ) {\n              activeItemRef.current = event.target;\n              syncTabStops();\n            }\n          }}\n        >\n          {children ?? (\n            <>\n              <FilterBarChips />\n              <FilterAddMenu />\n              <FilterBarAdvanced />\n              <FilterBarClear />\n              <FilterBarUndo />\n            </>\n          )}\n        </div>\n        <span id={summaryId} hidden>\n          {describeFilter(state.filter, fields, describeOptions)}\n        </span>\n        <FilterBarRescue />\n        <LiveRegion slotName=\"filter-bar-status\">\n          {announcedCount === undefined\n            ? \"\"\n            : labels.resultCount(announcedCount)}\n          {rescueText ? ` ${rescueText}` : \"\"}\n        </LiveRegion>\n      </div>\n    </FilterBarContext.Provider>\n  );\n}\n"
    },
    {
      "path": "packages/components/src/components/filter-builder/index.ts",
      "type": "registry:file",
      "target": "~/components/dethink/components/filter-builder/index.ts",
      "content": "export type {\n  Filter,\n  BuiltInFilterFieldType,\n  FilterCombinator,\n  FilterCondition,\n  FilterDate,\n  FilterDateUnit,\n  FilterDiff,\n  FilterDuration,\n  FilterEvaluateOptions,\n  FilterEvaluationContext,\n  FilterField,\n  FilterFieldType,\n  FilterFieldTypeDefinition,\n  FilterFields,\n  FilterGroup,\n  FilterIssue,\n  FilterIssueCode,\n  FilterNode,\n  FilterOperatorArity,\n  FilterOperatorDefinition,\n  FilterOption,\n  FilterValue,\n  FilterValueEditorRenderProps,\n  FilterWeekday,\n} from \"./filter-types\";\nexport {\n  isEmptyFilterOperator,\n  isEmptyFilterValue,\n  isNotEmptyFilterOperator,\n  multiOptionFilterOperators,\n  optionFilterOperators,\n  textFilterOperators,\n} from \"./filter-operators\";\nexport {\n  booleanFilterOperators,\n  dateFilterOperators,\n  numberFilterOperators,\n} from \"./filter-typed-operators\";\nexport {\n  builtInFilterFieldTypes,\n  defaultFilterOperatorByType,\n  defineFilterFieldType,\n  defineFilterOperator,\n  filterOperatorsByType,\n  resolveFilterFieldType,\n} from \"./filter-field-types\";\nexport {\n  calendarDayInZone,\n  createFilterEvaluationContext,\n  endOfCalendarPeriod,\n  formatFilterDate,\n  formatFilterDuration,\n  isCalendarDay,\n  isFilterDate,\n  isFilterDuration,\n  resolveFilterDate,\n  shiftCalendarDay,\n  startOfCalendarPeriod,\n  toCalendarDay,\n} from \"./filter-dates\";\nexport {\n  DEFAULT_FILTER_MAX_DEPTH,\n  addFilterNode,\n  canAddFilterGroup,\n  canWrapFilterNode,\n  clearFilter,\n  computeFilterFacets,\n  computeFilterImpact,\n  countFilterMatches,\n  createFilter,\n  createFilterCondition,\n  createFilterId,\n  createFilterPredicate,\n  defaultFilterDescribeLabels,\n  defineFilterFields,\n  describeFilter,\n  describeFilterCondition,\n  diffFilter,\n  evaluateFilter,\n  findFilterNode,\n  findFilterParent,\n  findFilterRescue,\n  formatFilterValue,\n  getDefaultFilterOperator,\n  getFilterConditions,\n  getFilterField,\n  getFilterHeight,\n  getFilterNodeDepth,\n  getFilterOperator,\n  getFilterOperatorLabel,\n  getFilterOperators,\n  isFilterConditionActive,\n  isFilterValueUsable,\n  isFilterEmpty,\n  normalizeFilter,\n  parseFilterQuery,\n  printFilterQuery,\n  getFilterQueryDefaultField,\n  toFilterJsonSchema,\n  getFilterFacetFilter,\n  getFilterFieldSummaries,\n  sanitizeFilterAssistantResult,\n  sanitizeFilterText,\n  getFilterProposalChanges,\n  applyFilterProposalChanges,\n  matchFilterProposalIds,\n  encodeFilterParam,\n  decodeFilterParam,\n  readFilterParam,\n  renameFilterField,\n  formatFilterSearch,\n  isSameFilterSearch,\n  getFilterSignature,\n  reconcileFilterIds,\n  getFilterQuerySegments,\n  getFilterQuerySuggestions,\n  getFilterQueryToken,\n  lexFilterQuery,\n  readFilterFieldValue,\n  moveFilterNode,\n  removeFilterNode,\n  shiftFilterNode,\n  toTanstackFilterFn,\n  unwrapFilterGroup,\n  updateFilterCondition,\n  updateFilterGroup,\n  validateFilter,\n  wrapFilterNode,\n  type CreateFilterPredicateOptions,\n  type DescribeFilterOptions,\n  type FilterFacetOptions,\n  type FilterFacetTarget,\n  type FilterRescue,\n  type FilterDescribeLabels,\n  type FilterAssistantResult,\n  type FilterClarification,\n  type FilterFieldSummary,\n  type FilterJsonSchema,\n  type FilterProposalChange,\n  type FilterProposalChangeKind,\n  type FilterUnresolved,\n  type FilterUnresolvedReason,\n  type SanitizeFilterAssistantOptions,\n  type SanitizedFilterAssistantResult,\n  type FilterParamError,\n  type FilterParamErrorCode,\n  type FilterParamOptions,\n  type FilterParamResult,\n  type FilterQueryError,\n  type FilterQueryErrorCode,\n  type FilterQueryOptions,\n  type FilterQueryParseResult,\n  type FilterQuerySegment,\n  type FilterQuerySegmentKind,\n  type FilterQuerySuggestion,\n  type FilterQueryToken,\n  type FilterQueryTokenKind,\n} from \"./filter-core\";\nexport {\n  useFilterState,\n  type FilterCommitOptions,\n  type FilterState,\n  type UseFilterStateOptions,\n} from \"./use-filter-state\";\nexport {\n  FilterFieldPicker,\n  FilterOperatorPicker,\n  filterListItemClassNames,\n  type FilterFieldPickerProps,\n  type FilterOperatorPickerProps,\n} from \"./filter-editors\";\nexport {\n  FilterValueEditor,\n  defaultFilterEditorLabels,\n  type FilterEditorLabels,\n  type FilterValueEditorProps,\n} from \"./filter-value-editor\";\nexport {\n  defaultFilterTypedEditorLabels,\n  type FilterTypedEditorLabels,\n} from \"./filter-typed-editors\";\nexport {\n  useFilterInsights,\n  type FilterInsights,\n  type UseFilterInsightsOptions,\n} from \"./use-filter-insights\";\nexport {\n  FilterBarRescue,\n  type FilterBarRescueProps,\n} from \"./filter-bar-rescue\";\nexport { FilterBar, type FilterBarProps } from \"./filter-bar\";\nexport {\n  FilterBarChips,\n  FilterBarClear,\n  FilterBarUndo,\n  type FilterBarActionProps,\n  type FilterBarChipsProps,\n} from \"./filter-bar-chips\";\nexport {\n  defaultFilterBarLabels,\n  filterBarActionClassNames,\n  filterBarClassNames,\n  filterChipClassNames,\n  useFilterBar,\n  type FilterBarLabels,\n  type FilterBarSize,\n} from \"./filter-bar-parts\";\nexport { FilterChip, type FilterChipProps } from \"./filter-chip\";\nexport { FilterAddMenu, type FilterAddMenuProps } from \"./filter-add-menu\";\nexport {\n  FilterGroupEditor,\n  type FilterGroupEditorProps,\n} from \"./filter-group-editor\";\nexport {\n  FilterBarAdvanced,\n  FilterGroupChip,\n  type FilterBarAdvancedProps,\n  type FilterGroupChipProps,\n} from \"./filter-group-chip\";\nexport {\n  useServerFacets,\n  type FilterFacetCounts,\n  type FilterFacetRequest,\n  type FilterFacetStatus,\n  type GetFilterFacets,\n  type ServerFacets,\n} from \"./use-server-facets\";\n"
    },
    {
      "path": "packages/components/src/utils/cn.ts",
      "type": "registry:file",
      "target": "~/components/dethink/utils/cn.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
    }
  ]
}
