1import {
2 createContext,
3 use,
4 useImperativeHandle,
5 useLayoutEffect,
6 type ReactNode,
7 type ComponentProps,
8 type Ref,
9} from "react";
10import { useThreadFind } from "./useThreadFind";
11import { ThreadFindBar } from "./ThreadFindBar";
12import { ChatCanvas } from "./ChatCanvas";
13
14type Find = ReturnType<typeof useThreadFind>;
15export type ThreadFindControls = Pick<Find, "open" | "close">;
16export const ThreadFindTimelineContext = createContext<Find["timelineProps"] | null>(null);
17const ThreadFindBarContext = createContext<Find["barProps"] | null>(null);
18
19export function ThreadFindCanvas({
20 findOptions,
21 controlsRef,
22 onOpenChange,
23 ...canvasProps
24}: ComponentProps<typeof ChatCanvas> & {
25 findOptions: Parameters<typeof useThreadFind>[0];
26 controlsRef: Ref<ThreadFindControls>;
27 onOpenChange: (open: boolean) => void;
28}) {
29 return (
30 <ThreadFindProvider {...findOptions} controlsRef={controlsRef} onOpenChange={onOpenChange}>
31 <ChatCanvas {...canvasProps} />
32 </ThreadFindProvider>
33 );
34}
35
36/** Owns search updates below the chat view so cycling does not rerender the composer and panels. */
37export function ThreadFindProvider({
38 controlsRef,
39 onOpenChange,
40 children,
41 ...options
42}: Parameters<typeof useThreadFind>[0] & {
43 controlsRef: Ref<ThreadFindControls>;
44 onOpenChange: (open: boolean) => void;
45 children: ReactNode;
46}) {
47 const find = useThreadFind(options);
48 useImperativeHandle(controlsRef, () => ({ open: find.open, close: find.close }), [
49 find.open,
50 find.close,
51 ]);
52 // Before paint, so the details card makes room in the same frame the bar appears.
53 useLayoutEffect(() => onOpenChange(find.isOpen), [find.isOpen, onOpenChange]);
54 return (
55 <ThreadFindBarContext value={find.barProps}>
56 <ThreadFindTimelineContext value={find.timelineProps}>{children}</ThreadFindTimelineContext>
57 </ThreadFindBarContext>
58 );
59}
60
61export function ThreadFind({ onClose }: { onClose: () => void }) {
62 const bar = use(ThreadFindBarContext);
63 if (!bar) return null;
64 return (
65 <ThreadFindBar
66 {...bar}
67 onClose={() => {
68 bar.onClose();
69 onClose();
70 }}
71 />
72 );
73}