diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-03-25 16:47:22 +0100 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-03-29 15:48:22 +0200 |
| commit | 8db3bead13441a5882d8689dfc6a9f09c82375d9 (patch) | |
| tree | e9d0cafa93904ffcbec3188c37961bcc16c6422d /apps/web/src/lib/selection/selectable-item.tsx | |
| parent | 39ea298a9ccba3104c549c0ca079c231981811f5 (diff) | |
| download | opencut-8db3bead13441a5882d8689dfc6a9f09c82375d9.tar.gz | |
feat: masks, properties refactor, shaders, storage migrations, and more
Diffstat (limited to 'apps/web/src/lib/selection/selectable-item.tsx')
| -rw-r--r-- | apps/web/src/lib/selection/selectable-item.tsx | 107 |
1 files changed, 107 insertions, 0 deletions
diff --git a/apps/web/src/lib/selection/selectable-item.tsx b/apps/web/src/lib/selection/selectable-item.tsx new file mode 100644 index 00000000..1d7e74a8 --- /dev/null +++ b/apps/web/src/lib/selection/selectable-item.tsx @@ -0,0 +1,107 @@ +"use client"; + +import { forwardRef, useCallback } from "react"; +import { useSelectionContext } from "@/lib/selection/context"; +import { SELECTABLE_ITEM_ATTRIBUTE } from "@/lib/selection/constants"; +import type { SelectableItemProps } from "@/lib/selection/types"; +import { cn } from "@/utils/ui"; + +function setForwardedRef<T>(ref: React.ForwardedRef<T>, value: T | null) { + if (typeof ref === "function") { + ref(value); + return; + } + + if (ref) { + ref.current = value; + } +} + +export const SelectableItem = forwardRef<HTMLDivElement, SelectableItemProps>( + function SelectableItem( + { + id, + children, + className, + onClick, + onKeyDown, + onMouseDown, + onContextMenu, + tabIndex, + ...rest + }: SelectableItemProps, + forwardedRef, + ) { + const { + highlightedId, + isBoxSelecting, + isSelected, + handleItemClick, + handleItemMouseDown, + registerItem, + } = useSelectionContext(); + const isItemSelected = isSelected(id); + const isItemHighlighted = highlightedId === id; + const stateClassName = cn( + "relative", + isBoxSelecting && "pointer-events-none", + isItemSelected && "ring-1 ring-primary rounded-sm bg-primary/10", + isItemHighlighted && + (isItemSelected + ? "rounded-sm shadow-[0_0_0_1px_hsl(var(--primary))]" + : "ring-1 ring-primary/60 rounded-sm bg-primary/5"), + ); + + const handleRef = useCallback( + (element: HTMLDivElement | null) => { + registerItem(id, element); + setForwardedRef(forwardedRef, element); + }, + [forwardedRef, id, registerItem], + ); + + return ( + <div + ref={handleRef} + className={cn(stateClassName, className)} + {...{ [SELECTABLE_ITEM_ATTRIBUTE]: "true" }} + role="option" + aria-selected={isItemSelected} + tabIndex={tabIndex ?? 0} + onClick={(event) => { + onClick?.(event); + if (event.defaultPrevented) { + return; + } + + handleItemClick({ event, id }); + }} + onMouseDown={(event) => { + onMouseDown?.(event); + if (event.defaultPrevented) { + return; + } + + handleItemMouseDown({ event, id }); + }} + onKeyDown={(event) => { + onKeyDown?.(event); + if (event.defaultPrevented || event.target !== event.currentTarget) { + return; + } + + if (event.key !== "Enter" && event.key !== " ") { + return; + } + + event.preventDefault(); + handleItemClick({ event, id }); + }} + onContextMenu={onContextMenu} + {...rest} + > + {children} + </div> + ); + }, +); |
