{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"event-calendar-event","type":"registry:ui","title":"The reusable event chip/bar/block - selection, clicks, drag + resize wiring, and the consumer render slot.","description":"The reusable event chip/bar/block - selection, clicks, drag + resize wiring, and the consumer render slot.","dependencies":["date-fns","radix-ui"],"registryDependencies":["@neui/event-calendar","@neui/event-calendar-dnd","@neui/event-calendar-lib","@neui/event-calendar-types","tooltip"],"files":[{"path":"event-calendar-event.tsx","type":"registry:ui","content":"// Title: Event Calendar Event\n// Description: The reusable event chip/bar/block - selection, clicks, drag + resize wiring, and the consumer render slot.\n\n\"use client\"\n\nimport {\n createContext,\n useContext,\n useMemo,\n type ButtonHTMLAttributes,\n type CSSProperties,\n type ReactNode,\n} from \"react\"\nimport {\n useEventCalendar,\n useEventCalendarSelector,\n useEventCalendarViewConfig,\n useEventCalendarViewContext,\n} from \"@/components/neui/event-calendar/event-calendar\"\nimport {\n markChipPress,\n useEventCalendarGestures,\n wasRecentDrag,\n} from \"@/components/neui/event-calendar/event-calendar-dnd\"\nimport {\n spansMultipleDays,\n toZoned,\n zonedStartOfDay,\n} from \"@/components/neui/event-calendar/event-calendar-lib\"\nimport type {\n EventCalendarOccurrence,\n EventCalendarSegment,\n} from \"@/components/neui/event-calendar/event-calendar-types\"\nimport { addDays, format } from \"date-fns\"\nimport { Slot } from \"radix-ui\"\n\nimport { cn } from \"@/lib/utils\"\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 event colors; every entry works on\n * light and dark surfaces through the chip's alpha background + accent border.\n */\nconst EVENT_CALENDAR_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\n/**\n * Standardized drag-ghost surface treatment, shared verbatim by every view\n * (month, week/day/N-days, resource). One visual language for interactions:\n * - move: the event is CARRIED FREELY - a cursor-attached full clone (built\n * by the dnd engine, data-slot=event-calendar-drag-carry) travels with the\n * pointer; the in-grid ghost is only this faint dashed placeholder marking\n * the snapped drop slot. The source stays dimmed in place.\n * - resize: the event is STRETCHED - the chip itself at the proposed extent\n * with a dashed boundary instead of solid (slight indicator, no elevation).\n * - invalid: destructive tint on the placeholder / dashed clone; the engine\n * adds the not-allowed cursor, a destructive ring on the carry clone, and\n * a cursor-following validation hint.\n */\nconst EVENT_CALENDAR_GHOST = {\n move: \"rounded-sm border border-dashed border-(--ec-event-color)/50 bg-(--ec-event-color)/8\",\n resize:\n \"rounded-sm border border-dashed border-(--ec-event-color)/70 overflow-hidden\",\n invalid: \"border-destructive/70 bg-destructive/10\",\n invalidResize: \"border-destructive/70\",\n /** Applied to the clone inside an invalid resize ghost. */\n invalidContent: \"opacity-60\",\n} as const\n\n/**\n * Fade-out truncation for stacked timed blocks: squeezed cascade columns\n * hard-clip titles into a mash of adjacent glyphs; a right-edge mask fade\n * reads cleaner than an ellipsis at those tiny widths. The mask applies ONLY\n * below a 10rem container width - mask-image forces text off subpixel\n * antialiasing onto a grayscale raster layer, so masking every wide chip\n * makes the whole grid read bolder/blurry and shimmer while the window\n * resizes. Wide chips keep a plain ellipsis. Consumer renderEvent content\n * can import and reuse it.\n */\nconst EVENT_CALENDAR_FADE_TRUNCATE =\n \"w-full truncate @max-[10rem]:text-clip @max-[10rem]:[mask-image:linear-gradient(to_right,#000_calc(100%-0.75rem),transparent)] @max-[10rem]:rtl:[mask-image:linear-gradient(to_left,#000_calc(100%-0.75rem),transparent)]\"\n\ninterface EventCalendarChipContextValue {\n occurrence: EventCalendarOccurrence\n segment: EventCalendarSegment\n isDragging: boolean\n isSelected: boolean\n}\n\nconst EventCalendarChipContext =\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n createContext | null>(null)\n\n/** The chip's subject; usable inside renderEvent content and chip children. */\nfunction useEventCalendarEventChip<\n TData = unknown,\n>(): EventCalendarChipContextValue {\n const ctx = useContext(EventCalendarChipContext)\n if (!ctx) {\n throw new Error(\n \"useEventCalendarEventChip must be used within \"\n )\n }\n return ctx as EventCalendarChipContextValue\n}\n\ninterface EventCalendarEventProps extends Omit<\n ButtonHTMLAttributes,\n \"children\"\n> {\n segment: EventCalendarSegment\n /** Replaces the default chip CONTENT; the wrapper stays calendar-owned. */\n children?: ReactNode\n /**\n * Static drag clone: renders the chip exactly as-is but inert - no gestures,\n * resize handles, selection/drag state, focus, or pointer events. Used for\n * the full-fidelity ghost that tracks the proposed slot during a move.\n */\n preview?: boolean\n asChild?: boolean\n}\n\n/**\n * The one interactive event element used by every view. The wrapper owns\n * positioning hooks, a11y, selection, drag/resize listeners, and data\n * attributes; content comes from children, the root renderEvent override,\n * or the built-in default.\n */\nfunction EventCalendarEvent({\n segment,\n className,\n asChild = false,\n children,\n preview = false,\n style,\n onPointerDown,\n onClick,\n onDoubleClick,\n ...props\n}: EventCalendarEventProps) {\n const instance = useEventCalendar()\n const viewConfig = useEventCalendarViewConfig()\n const { view } = useEventCalendarViewContext()\n const gestures = useEventCalendarGestures()\n const { settings } = instance\n const occurrence = segment.occurrence\n const event = occurrence.event\n\n const isSelectedRaw = useEventCalendarSelector(\n (state) => state.selection.eventKeys.includes(occurrence.key),\n { calendar: instance }\n )\n const isDraggingRaw = useEventCalendarSelector(\n (state) => state.drag?.occurrence.key === occurrence.key,\n { calendar: instance }\n )\n // reactive, unlike gestures.canResize: api.setInteractions({ resize })\n // must add/remove the handles without waiting for an unrelated re-render\n const resizeOn = useEventCalendarSelector(\n (state) => state.interactions.resize,\n { calendar: instance }\n )\n // A preview clone must never inherit the source's selected/dragging state\n // (the drag key matches, which would dim the clone itself).\n const isSelected = preview ? false : isSelectedRaw\n const isDragging = preview ? false : isDraggingRaw\n\n const isBar =\n occurrence.allDay || spansMultipleDays(occurrence, settings.timeZone)\n const inTimeGrid =\n view === \"week\" || view === \"day\" || view === \"days\" || view === \"resource\"\n const interactive = view !== \"agenda\" && !preview\n const timedBlock = inTimeGrid && !isBar\n const horizontalBar = isBar && !inTimeGrid\n // >= compactEventMinutes renders the stacked (title over time) layout;\n // squeezed cascade columns there fade-truncate instead of hard-clipping\n // into neighbors\n const stackedBlock =\n timedBlock &&\n (segment.endMin ?? 0) - (segment.startMin ?? 0) >=\n viewConfig.compactEventMinutes\n\n const defaultContent = (\n <>\n {/* leading color dot for single-row chips (month cells, all-day bars);\n time-grid blocks read their color from the tinted surface instead -\n in the stacked layout a dot would sit alone on the first line */}\n {!timedBlock && (\n \n )}\n {occurrence.isRecurring && (\n \n )}\n \n {event.title}\n \n {/* month cells are narrow: a compact never-shrinking start time keeps\n the title readable; grid views show the full range */}\n {!occurrence.allDay &&\n segment.isStart &&\n (view === \"month\" ? (\n \n {format(\n toZoned(occurrence.start, settings.timeZone),\n settings.i18n.formats.eventTime,\n { locale: settings.locale }\n )}\n \n ) : (\n