{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"gantt-bar","type":"registry:ui","title":"The interactive gantt bar - selection, clicks, drag + resize wiring, and the consumer render slot.","description":"The interactive gantt bar - selection, clicks, drag + resize wiring, and the consumer render slot.","dependencies":["radix-ui"],"registryDependencies":["context-menu","@neui/gantt","@neui/gantt-dnd","@neui/gantt-lib","@neui/gantt-types","tooltip"],"files":[{"path":"gantt-bar.tsx","type":"registry:ui","content":"// Title: Gantt Bar\n// Description: The interactive gantt bar - selection, clicks, drag + resize wiring, and the consumer render slot.\n\n\"use client\"\n\nimport {\n createContext,\n useContext,\n useMemo,\n useState,\n type ButtonHTMLAttributes,\n type CSSProperties,\n type ReactNode,\n} from \"react\"\nimport {\n useGantt,\n useGanttSelector,\n useGanttViewConfig,\n} from \"@/components/neui/gantt/gantt\"\nimport {\n useGanttGestures,\n wasRecentDrag,\n} from \"@/components/neui/gantt/gantt-dnd\"\nimport {\n flattenResources,\n toZoned,\n} from \"@/components/neui/gantt/gantt-lib\"\nimport type {\n GanttOccurrence,\n GanttSegment,\n} from \"@/components/neui/gantt/gantt-types\"\nimport { Slot } from \"radix-ui\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n ContextMenu,\n ContextMenuContent,\n ContextMenuTrigger,\n} from \"@/components/ui/context-menu\"\nimport {\n Tooltip,\n TooltipContent,\n TooltipProvider,\n TooltipTrigger,\n} from \"@/components/ui/tooltip\"\nimport { IconPlaceholder } from \"@/app/(create)/components/icon-placeholder\"\n\n/**\n * Effective Tailwind palette presets for bar colors; every entry works on\n * light and dark surfaces through the bar's alpha background + accent border.\n */\nconst GANTT_COLORS: Array<{ name: string; value: string }> = [\n { name: \"Blue\", value: \"var(--color-blue-500)\" },\n { name: \"Emerald\", value: \"var(--color-emerald-500)\" },\n { name: \"Violet\", value: \"var(--color-violet-500)\" },\n { name: \"Rose\", value: \"var(--color-rose-500)\" },\n { name: \"Amber\", value: \"var(--color-amber-500)\" },\n { name: \"Cyan\", value: \"var(--color-cyan-500)\" },\n { name: \"Orange\", value: \"var(--color-orange-500)\" },\n { name: \"Pink\", value: \"var(--color-pink-500)\" },\n { name: \"Teal\", value: \"var(--color-teal-500)\" },\n { name: \"Indigo\", value: \"var(--color-indigo-500)\" },\n]\n\ninterface GanttBarContextValue {\n occurrence: GanttOccurrence\n segment: GanttSegment\n isDragging: boolean\n isSelected: boolean\n}\n\nconst GanttBarContext =\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n createContext | null>(null)\n\n/** The bar's subject; usable inside renderEvent content and bar children. */\nfunction useGanttBarContext(): GanttBarContextValue {\n const ctx = useContext(GanttBarContext)\n if (!ctx) {\n throw new Error(\"useGanttBarContext must be used within \")\n }\n return ctx as GanttBarContextValue\n}\n\ninterface GanttBarProps extends Omit<\n ButtonHTMLAttributes,\n \"children\"\n> {\n segment: GanttSegment\n /** Replaces the default bar CONTENT; the wrapper stays gantt-owned. */\n children?: ReactNode\n /**\n * The title renders beside the bar (view-owned), so the default inner\n * content is suppressed. Explicit children and renderEvent still win.\n */\n labelOutside?: boolean\n /**\n * The owning row's title for the aria-label. Pass it when the row is in\n * scope (the internal view does); omitting falls back to a tree lookup.\n */\n rowTitle?: string\n asChild?: boolean\n}\n\n/**\n * The one interactive bar element. The wrapper owns positioning hooks, a11y,\n * selection, drag/resize listeners, and data attributes; content comes from\n * children, the root renderEvent override, or the built-in default.\n */\nfunction GanttBar({\n segment,\n className,\n asChild = false,\n children,\n labelOutside,\n rowTitle: rowTitleProp,\n ...props\n}: GanttBarProps) {\n const instance = useGantt()\n const viewConfig = useGanttViewConfig()\n const gestures = useGanttGestures()\n const { settings } = instance\n const occurrence = segment.occurrence\n const event = occurrence.event\n\n const isSelected = useGanttSelector(\n (state) => state.selection.eventKeys.includes(occurrence.key),\n { calendar: instance }\n )\n const isDragging = useGanttSelector(\n (state) => state.drag?.occurrence.key === occurrence.key,\n { calendar: instance }\n )\n // Which gesture owns this bar: a move hides the original (the smooth clone\n // stands in for it); a resize keeps it as a faint placeholder behind the\n // dashed preview so you can see the original extent.\n const dragKind = useGanttSelector(\n (state) =>\n state.drag?.occurrence.key === occurrence.key ? state.drag.kind : null,\n { calendar: instance }\n )\n // Hover-only range tooltip. Focus opens are ignored (the known button+\n // tooltip flash: clicking a bar opens a dialog, focus returns, and a\n // focus-triggered tooltip would pop). Hidden while dragging/resizing.\n const [tipOpen, setTipOpen] = useState(false)\n // Gated on tipOpen: with the tooltip closed the selector returns a stable\n // false, so gesture start/end doesn't re-render every mounted bar.\n const anyInteracting = useGanttSelector(\n (state) => tipOpen && (state.drag !== null || state.slotDraft !== null),\n { calendar: instance }\n )\n\n const progress =\n typeof event.progress === \"number\"\n ? Math.min(Math.max(Math.round(event.progress), 0), 100)\n : null\n\n const defaultContent = (\n <>\n {occurrence.isRecurring && (\n \n )}\n {event.title}\n {!occurrence.allDay && segment.isStart && (\n \n )}\n \n )\n\n const renderProps = { occurrence, segment, isDragging, isSelected }\n const content =\n children ??\n viewConfig.renderEvent?.(renderProps) ??\n (labelOutside ? null : defaultContent)\n // Consumer-owned content owns the WHOLE inner visualization: the built-in\n // progress fill and done mark yield so custom bars start from a blank\n // canvas (progress stays readable via data-progress/data-completed).\n const consumerOwnsContent = children !== undefined || !!viewConfig.renderEvent\n\n const timeLabel = settings.i18n.functions.formatEventTime(\n toZoned(occurrence.start, settings.timeZone),\n toZoned(occurrence.end, settings.timeZone),\n occurrence.allDay,\n settings.locale\n )\n // name the row too: the split-pane layout carries no grid semantics.\n // The prop path is O(1); the lookup fallback is memoized so external\n // GanttBar usage never flattens the tree per render.\n const fallbackRowTitle = useMemo(\n () =>\n rowTitleProp === undefined && event.resourceId\n ? flattenResources(settings.resources).find(\n ({ resource }) => resource.id === event.resourceId\n )?.resource.title\n : undefined,\n [rowTitleProp, event.resourceId, settings.resources]\n )\n const rowTitle = rowTitleProp ?? fallbackRowTitle\n\n const showResize = gestures.canResize(segment)\n const resizeHandles = showResize && (\n <>\n {segment.isStart && (\n gestures.beginResize(e, segment, \"start\")}\n >\n \n \n )}\n {segment.isEnd && (\n gestures.beginResize(e, segment, \"end\")}\n >\n \n \n )}\n \n )\n\n const Comp = asChild ? Slot.Root : \"button\"\n\n const barButton = (\n {\n e.stopPropagation()\n gestures.beginMove(e, segment)\n }}\n onClick={(e: React.MouseEvent) => {\n e.stopPropagation()\n if (wasRecentDrag()) return\n instance.api.selectEvent(occurrence.key)\n settings.onEventClick?.(occurrence, e)\n }}\n onDoubleClick={(e: React.MouseEvent) => {\n e.stopPropagation()\n settings.onEventDoubleClick?.(occurrence, e)\n }}\n className={cn(\n \"group/gantt-bar-group text-foreground @container relative flex w-full min-w-0 cursor-pointer touch-none items-center gap-1.5 overflow-hidden rounded-sm px-1.5 py-0.5 text-start leading-normal select-none\",\n \"focus-visible:ring-ring/50 outline-none focus-visible:ring-2\",\n // the unfilled remainder has to be legible on its own - at /12 a bar\n // with a progress fill read as a floating segment with no basement\n \"bg-(--gantt-event-color)/20 hover:bg-(--gantt-event-color)/30\",\n // move: hide the original (the smooth cursor clone represents it)\n \"data-[drag-kind=move]:opacity-0\",\n // resize: keep the original event exactly, just fade it to a soft\n // placeholder behind the dashed preview - no dramatic restyle\n \"data-[drag-kind=resize-end]:opacity-40 data-[drag-kind=resize-start]:opacity-40\",\n \"data-selected:bg-(--gantt-event-color)/30\",\n segment.continuesBefore && \"rounded-s-none\",\n segment.continuesAfter && \"rounded-e-none\",\n viewConfig.classNames?.event,\n className\n )}\n {...props}\n >\n {progress !== null && (\n // Chrome, not content: it is an absolutely-positioned layer BEHIND\n // whatever the bar renders, so a consumer bar (renderEvent) keeps its\n // completion fill instead of silently losing it. The inline done-mark\n // below stays gated, because that one really is content.\n \n )}\n {progress === 100 && !consumerOwnsContent && (\n // done mark: completion chrome like the fill itself, so it shows\n // for outside-label bars too (where the inner content is empty)\n \n )}\n {content}\n {resizeHandles}\n \n )\n\n // Consumer-owned right-click menu (headless): the primitive only wires the\n // ContextMenu; the items and their handlers come entirely from the block.\n const menu = viewConfig.renderEventMenu?.(renderProps)\n\n // The bar is simultaneously the tooltip trigger and (when a menu exists)\n // the context-menu trigger; Radix composes both via asChild. The tooltip\n // opens only on hover: Radix has no open reason, so focus opens (the known\n // button+tooltip flash) are suppressed at the trigger via preventDefault,\n // and press already closes the tooltip in Radix.\n const trigger = menu ? (\n \n e.preventDefault()}>\n {barButton}\n \n \n ) : (\n e.preventDefault()}>\n {barButton}\n \n )\n\n const barTree = (\n \n setTipOpen(next)}\n >\n {trigger}\n {tipOpen && !anyInteracting && (\n \n
{event.title}
\n
{timeLabel}
\n
\n )}\n \n
\n )\n\n return (\n \n {menu ? (\n \n {barTree}\n \n {menu}\n \n \n ) : (\n barTree\n )}\n \n )\n}\n\nexport { GANTT_COLORS, GanttBar, useGanttBarContext }\nexport type { GanttBarContextValue, GanttBarProps }","target":"components/neui/gantt/gantt-bar.tsx"}]}