Files
next-tjwater-drainage-frontend/shared/ai-elements/suggestion.tsx
T

119 lines
3.2 KiB
TypeScript

"use client";
import type { ComponentProps } from "react";
import { useCallback, useEffect, useRef } from "react";
import { cn } from "@/lib/utils";
import { Button } from "@/shared/ui/button";
export type SuggestionsProps = ComponentProps<"div">;
const SCROLL_EASING = 0.28;
const SCROLL_END_THRESHOLD = 0.5;
export const Suggestions = ({ className, children, ...props }: SuggestionsProps) => {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const element = containerRef.current;
if (!element) {
return;
}
let animationFrameId: number | null = null;
let targetScrollLeft = element.scrollLeft;
const animateScroll = () => {
const distance = targetScrollLeft - element.scrollLeft;
if (Math.abs(distance) <= SCROLL_END_THRESHOLD) {
element.scrollLeft = targetScrollLeft;
animationFrameId = null;
return;
}
element.scrollLeft += distance * SCROLL_EASING;
animationFrameId = window.requestAnimationFrame(animateScroll);
};
const handleWheel = (event: globalThis.WheelEvent) => {
const maxScrollLeft = element.scrollWidth - element.clientWidth;
const rawDelta = Math.abs(event.deltaY) >= Math.abs(event.deltaX) ? event.deltaY : event.deltaX;
const delta =
event.deltaMode === 1
? rawDelta * 16
: event.deltaMode === 2
? rawDelta * element.clientWidth
: rawDelta;
const canScroll = delta > 0 ? element.scrollLeft < maxScrollLeft : element.scrollLeft > 0;
if (maxScrollLeft <= 0 || delta === 0 || !canScroll) {
return;
}
event.preventDefault();
targetScrollLeft = Math.min(maxScrollLeft, Math.max(0, targetScrollLeft + delta));
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
element.scrollLeft = targetScrollLeft;
return;
}
if (animationFrameId === null) {
animationFrameId = window.requestAnimationFrame(animateScroll);
}
};
element.addEventListener("wheel", handleWheel, { passive: false });
return () => {
element.removeEventListener("wheel", handleWheel);
if (animationFrameId !== null) {
window.cancelAnimationFrame(animationFrameId);
}
};
}, []);
return (
<div
ref={containerRef}
className={cn(
"flex w-full flex-nowrap items-center gap-2 overflow-x-auto overscroll-x-contain pb-0.5 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
className
)}
{...props}
>
{children}
</div>
);
};
export type SuggestionProps = Omit<ComponentProps<typeof Button>, "onClick"> & {
suggestion: string;
onClick?: (suggestion: string) => void;
};
export const Suggestion = ({
suggestion,
onClick,
className,
variant = "outline",
size = "sm",
children,
...props
}: SuggestionProps) => {
const handleClick = useCallback(() => {
onClick?.(suggestion);
}, [onClick, suggestion]);
return (
<Button
className={cn("shrink-0 cursor-pointer rounded-full px-3.5", className)}
onClick={handleClick}
size={size}
type="button"
variant={variant}
{...props}
>
{children || suggestion}
</Button>
);
};