{
  "name": "timeline",
  "type": "registry:ui",
  "dependencies": [
    "radix-ui"
  ],
  "registryDependencies": [
    "@diceui/use-isomorphic-layout-effect",
    "@diceui/use-lazy-ref"
  ],
  "files": [
    {
      "path": "ui/timeline.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport {\n  Direction as DirectionPrimitive,\n  Slot as SlotPrimitive,\n} from \"radix-ui\";\nimport * as React from \"react\";\n\nimport { useComposedRefs } from \"@/lib/compose-refs\";\nimport { cn } from \"@/lib/utils\";\nimport { useIsomorphicLayoutEffect } from \"@/registry/bases/radix/hooks/use-isomorphic-layout-effect\";\nimport { useLazyRef } from \"@/registry/bases/radix/hooks/use-lazy-ref\";\n\ntype Direction = \"ltr\" | \"rtl\";\ntype Orientation = \"vertical\" | \"horizontal\";\ntype Variant = \"default\" | \"alternate\";\ntype Status = \"completed\" | \"active\" | \"pending\";\n\ninterface DivProps extends React.ComponentProps<\"div\"> {\n  asChild?: boolean;\n}\n\ntype ItemElement = React.ComponentRef<typeof TimelineItem>;\n\nconst ROOT_NAME = \"Timeline\";\nconst ITEM_NAME = \"TimelineItem\";\nconst DOT_NAME = \"TimelineDot\";\nconst CONNECTOR_NAME = \"TimelineConnector\";\nconst CONTENT_NAME = \"TimelineContent\";\n\nfunction getItemStatus(itemIndex: number, activeIndex?: number): Status {\n  if (activeIndex === undefined) return \"pending\";\n  if (itemIndex < activeIndex) return \"completed\";\n  if (itemIndex === activeIndex) return \"active\";\n  return \"pending\";\n}\n\nfunction getSortedEntries(\n  entries: [string, React.RefObject<ItemElement | null>][],\n) {\n  return entries.sort((a, b) => {\n    const elementA = a[1].current;\n    const elementB = b[1].current;\n    if (!elementA || !elementB) return 0;\n    const position = elementA.compareDocumentPosition(elementB);\n    if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1;\n    if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1;\n    return 0;\n  });\n}\n\nfunction useStore<T>(selector: (store: Store) => T): T {\n  const store = React.useContext(StoreContext);\n  if (!store) {\n    throw new Error(`\\`useStore\\` must be used within \\`${ROOT_NAME}\\``);\n  }\n\n  const getSnapshot = React.useCallback(\n    () => selector(store),\n    [store, selector],\n  );\n\n  return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);\n}\n\ninterface StoreState {\n  items: Map<string, React.RefObject<ItemElement | null>>;\n}\n\ninterface Store {\n  subscribe: (callback: () => void) => () => void;\n  getState: () => StoreState;\n  notify: () => void;\n  onItemRegister: (\n    id: string,\n    ref: React.RefObject<ItemElement | null>,\n  ) => void;\n  onItemUnregister: (id: string) => void;\n  getNextItemStatus: (id: string, activeIndex?: number) => Status | undefined;\n  getItemIndex: (id: string) => number;\n}\n\nconst StoreContext = React.createContext<Store | null>(null);\n\nfunction useStoreContext(consumerName: string) {\n  const context = React.useContext(StoreContext);\n  if (!context) {\n    throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n  }\n  return context;\n}\n\ninterface TimelineContextValue {\n  dir: Direction;\n  orientation: Orientation;\n  variant: Variant;\n  activeIndex?: number;\n}\n\nconst TimelineContext = React.createContext<TimelineContextValue | null>(null);\n\nfunction useTimelineContext(consumerName: string) {\n  const context = React.useContext(TimelineContext);\n  if (!context) {\n    throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n  }\n  return context;\n}\n\nconst timelineVariants = cva(\n  \"relative flex [--timeline-connector-thickness:0.125rem] [--timeline-dot-size:0.875rem]\",\n  {\n    variants: {\n      orientation: {\n        vertical: \"flex-col\",\n        horizontal: \"flex-row items-start\",\n      },\n      variant: {\n        default: \"\",\n        alternate: \"\",\n      },\n    },\n    compoundVariants: [\n      {\n        orientation: \"vertical\",\n        variant: \"default\",\n        class: \"gap-6\",\n      },\n      {\n        orientation: \"horizontal\",\n        variant: \"default\",\n        class: \"gap-8\",\n      },\n      {\n        orientation: \"vertical\",\n        variant: \"alternate\",\n        class: \"relative w-full gap-3\",\n      },\n      {\n        orientation: \"horizontal\",\n        variant: \"alternate\",\n        class: \"items-center gap-4\",\n      },\n    ],\n    defaultVariants: {\n      orientation: \"vertical\",\n      variant: \"default\",\n    },\n  },\n);\n\ninterface TimelineProps extends DivProps {\n  dir?: Direction;\n  orientation?: Orientation;\n  variant?: Variant;\n  activeIndex?: number;\n}\n\nfunction Timeline(props: TimelineProps) {\n  const {\n    orientation = \"vertical\",\n    variant = \"default\",\n    dir: dirProp,\n    activeIndex,\n    asChild,\n    className,\n    ...rootProps\n  } = props;\n\n  const dir = DirectionPrimitive.useDirection(dirProp);\n\n  const listenersRef = useLazyRef(() => new Set<() => void>());\n  const stateRef = useLazyRef<StoreState>(() => ({\n    items: new Map(),\n  }));\n\n  const store = React.useMemo<Store>(() => {\n    return {\n      subscribe: (cb) => {\n        listenersRef.current.add(cb);\n        return () => listenersRef.current.delete(cb);\n      },\n      getState: () => stateRef.current,\n      notify: () => {\n        for (const cb of listenersRef.current) {\n          cb();\n        }\n      },\n      onItemRegister: (\n        id: string,\n        ref: React.RefObject<ItemElement | null>,\n      ) => {\n        stateRef.current.items.set(id, ref);\n        store.notify();\n      },\n      onItemUnregister: (id: string) => {\n        stateRef.current.items.delete(id);\n        store.notify();\n      },\n      getNextItemStatus: (id: string, activeIndex?: number) => {\n        const entries = Array.from(stateRef.current.items.entries());\n        const sortedEntries = getSortedEntries(entries);\n\n        const currentIndex = sortedEntries.findIndex(([key]) => key === id);\n        if (currentIndex === -1 || currentIndex === sortedEntries.length - 1) {\n          return undefined;\n        }\n\n        const nextItemIndex = currentIndex + 1;\n        return getItemStatus(nextItemIndex, activeIndex);\n      },\n      getItemIndex: (id: string) => {\n        const entries = Array.from(stateRef.current.items.entries());\n        const sortedEntries = getSortedEntries(entries);\n        return sortedEntries.findIndex(([key]) => key === id);\n      },\n    };\n  }, [listenersRef, stateRef]);\n\n  const contextValue = React.useMemo<TimelineContextValue>(\n    () => ({\n      dir,\n      orientation,\n      variant,\n      activeIndex,\n    }),\n    [dir, orientation, variant, activeIndex],\n  );\n\n  const RootPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <StoreContext.Provider value={store}>\n      <TimelineContext.Provider value={contextValue}>\n        <RootPrimitive\n          role=\"list\"\n          aria-orientation={orientation}\n          data-slot=\"timeline\"\n          data-orientation={orientation}\n          data-variant={variant}\n          dir={dir}\n          {...rootProps}\n          className={cn(timelineVariants({ orientation, variant, className }))}\n        />\n      </TimelineContext.Provider>\n    </StoreContext.Provider>\n  );\n}\n\ninterface TimelineItemContextValue {\n  id: string;\n  status: Status;\n  isAlternateRight: boolean;\n}\n\nconst TimelineItemContext =\n  React.createContext<TimelineItemContextValue | null>(null);\n\nfunction useTimelineItemContext(consumerName: string) {\n  const context = React.useContext(TimelineItemContext);\n  if (!context) {\n    throw new Error(`\\`${consumerName}\\` must be used within \\`${ITEM_NAME}\\``);\n  }\n  return context;\n}\n\nconst timelineItemVariants = cva(\"relative flex\", {\n  variants: {\n    orientation: {\n      vertical: \"\",\n      horizontal: \"\",\n    },\n    variant: {\n      default: \"\",\n      alternate: \"\",\n    },\n    isAlternateRight: {\n      true: \"\",\n      false: \"\",\n    },\n  },\n  compoundVariants: [\n    {\n      orientation: \"vertical\",\n      variant: \"default\",\n      class: \"gap-3 pb-8 last:pb-0\",\n    },\n    {\n      orientation: \"horizontal\",\n      variant: \"default\",\n      class: \"flex-col gap-3\",\n    },\n    {\n      orientation: \"vertical\",\n      variant: \"alternate\",\n      isAlternateRight: false,\n      class: \"w-1/2 gap-3 pr-6 pb-12 last:pb-0\",\n    },\n    {\n      orientation: \"vertical\",\n      variant: \"alternate\",\n      isAlternateRight: true,\n      class: \"ml-auto w-1/2 flex-row-reverse gap-3 pb-12 pl-6 last:pb-0\",\n    },\n    {\n      orientation: \"horizontal\",\n      variant: \"alternate\",\n      class: \"grid min-w-0 grid-rows-[1fr_auto_1fr] gap-3\",\n    },\n  ],\n  defaultVariants: {\n    orientation: \"vertical\",\n    variant: \"default\",\n    isAlternateRight: false,\n  },\n});\n\nfunction TimelineItem(props: DivProps) {\n  const { asChild, className, id, ref, ...itemProps } = props;\n\n  const { dir, orientation, variant, activeIndex } =\n    useTimelineContext(ITEM_NAME);\n  const store = useStoreContext(ITEM_NAME);\n\n  const instanceId = React.useId();\n  const itemId = id ?? instanceId;\n  const itemRef = React.useRef<ItemElement | null>(null);\n  const composedRef = useComposedRefs(ref, itemRef);\n\n  const itemIndex = useStore((state) => state.getItemIndex(itemId));\n\n  const status = React.useMemo<Status>(() => {\n    return getItemStatus(itemIndex, activeIndex);\n  }, [activeIndex, itemIndex]);\n\n  useIsomorphicLayoutEffect(() => {\n    store.onItemRegister(itemId, itemRef);\n    return () => {\n      store.onItemUnregister(itemId);\n    };\n  }, [id, store]);\n\n  const isAlternateRight = variant === \"alternate\" && itemIndex % 2 === 1;\n\n  const itemContextValue = React.useMemo<TimelineItemContextValue>(\n    () => ({ id: itemId, status, isAlternateRight }),\n    [itemId, status, isAlternateRight],\n  );\n\n  const ItemPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <TimelineItemContext.Provider value={itemContextValue}>\n      <ItemPrimitive\n        role=\"listitem\"\n        aria-current={status === \"active\" ? \"step\" : undefined}\n        data-slot=\"timeline-item\"\n        data-status={status}\n        data-orientation={orientation}\n        data-alternate-right={isAlternateRight ? \"\" : undefined}\n        id={itemId}\n        dir={dir}\n        {...itemProps}\n        ref={composedRef}\n        className={cn(\n          timelineItemVariants({\n            orientation,\n            variant,\n            isAlternateRight,\n            className,\n          }),\n        )}\n      />\n    </TimelineItemContext.Provider>\n  );\n}\n\nconst timelineContentVariants = cva(\"flex-1\", {\n  variants: {\n    orientation: {\n      vertical: \"\",\n      horizontal: \"\",\n    },\n    variant: {\n      default: \"\",\n      alternate: \"\",\n    },\n    isAlternateRight: {\n      true: \"\",\n      false: \"\",\n    },\n  },\n  compoundVariants: [\n    {\n      variant: \"alternate\",\n      orientation: \"vertical\",\n      isAlternateRight: false,\n      class: \"text-right\",\n    },\n    {\n      variant: \"alternate\",\n      orientation: \"horizontal\",\n      isAlternateRight: false,\n      class: \"row-start-3 pt-2\",\n    },\n    {\n      variant: \"alternate\",\n      orientation: \"horizontal\",\n      isAlternateRight: true,\n      class: \"row-start-1 pb-2\",\n    },\n  ],\n  defaultVariants: {\n    orientation: \"vertical\",\n    variant: \"default\",\n    isAlternateRight: false,\n  },\n});\n\nfunction TimelineContent(props: DivProps) {\n  const { asChild, className, ...contentProps } = props;\n\n  const { variant, orientation } = useTimelineContext(CONTENT_NAME);\n  const { status, isAlternateRight } = useTimelineItemContext(CONTENT_NAME);\n\n  const ContentPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <ContentPrimitive\n      data-slot=\"timeline-content\"\n      data-status={status}\n      {...contentProps}\n      className={cn(\n        timelineContentVariants({\n          orientation,\n          variant,\n          isAlternateRight,\n          className,\n        }),\n      )}\n    />\n  );\n}\n\nconst timelineDotVariants = cva(\n  \"relative z-10 flex size-[var(--timeline-dot-size)] shrink-0 items-center justify-center rounded-full border-2 bg-background\",\n  {\n    variants: {\n      status: {\n        completed: \"border-primary\",\n        active: \"border-primary\",\n        pending: \"border-border\",\n      },\n      orientation: {\n        vertical: \"\",\n        horizontal: \"\",\n      },\n      variant: {\n        default: \"\",\n        alternate: \"\",\n      },\n      isAlternateRight: {\n        true: \"\",\n        false: \"\",\n      },\n    },\n    compoundVariants: [\n      {\n        variant: \"alternate\",\n        orientation: \"vertical\",\n        isAlternateRight: false,\n        class:\n          \"absolute -right-[calc(var(--timeline-dot-size)/2-var(--timeline-connector-thickness)/2)] bg-background\",\n      },\n      {\n        variant: \"alternate\",\n        orientation: \"vertical\",\n        isAlternateRight: true,\n        class:\n          \"absolute -left-[calc(var(--timeline-dot-size)/2-var(--timeline-connector-thickness)/2)] bg-background\",\n      },\n      {\n        variant: \"alternate\",\n        orientation: \"horizontal\",\n        class: \"row-start-2 bg-background\",\n      },\n      {\n        variant: \"alternate\",\n        status: \"completed\",\n        class: \"bg-background\",\n      },\n      {\n        variant: \"alternate\",\n        status: \"active\",\n        class: \"bg-background\",\n      },\n    ],\n    defaultVariants: {\n      status: \"pending\",\n      orientation: \"vertical\",\n      variant: \"default\",\n      isAlternateRight: false,\n    },\n  },\n);\n\nfunction TimelineDot(props: DivProps) {\n  const { asChild, className, ...dotProps } = props;\n\n  const { orientation, variant } = useTimelineContext(DOT_NAME);\n  const { status, isAlternateRight } = useTimelineItemContext(DOT_NAME);\n\n  const DotPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <DotPrimitive\n      data-slot=\"timeline-dot\"\n      data-status={status}\n      data-orientation={orientation}\n      {...dotProps}\n      className={cn(\n        timelineDotVariants({\n          status,\n          orientation,\n          variant,\n          isAlternateRight,\n          className,\n        }),\n      )}\n    />\n  );\n}\n\nconst timelineConnectorVariants = cva(\"absolute z-0\", {\n  variants: {\n    isCompleted: {\n      true: \"bg-primary\",\n      false: \"bg-border\",\n    },\n    orientation: {\n      vertical: \"\",\n      horizontal: \"\",\n    },\n    variant: {\n      default: \"\",\n      alternate: \"\",\n    },\n    isAlternateRight: {\n      true: \"\",\n      false: \"\",\n    },\n  },\n  compoundVariants: [\n    {\n      orientation: \"vertical\",\n      variant: \"default\",\n      class:\n        \"start-[calc(var(--timeline-dot-size)/2-var(--timeline-connector-thickness)/2)] top-3 h-[calc(100%+0.5rem)] w-[var(--timeline-connector-thickness)]\",\n    },\n    {\n      orientation: \"horizontal\",\n      variant: \"default\",\n      class:\n        \"start-3 top-[calc(var(--timeline-dot-size)/2-var(--timeline-connector-thickness)/2)] h-[var(--timeline-connector-thickness)] w-[calc(100%+0.5rem)]\",\n    },\n    {\n      orientation: \"vertical\",\n      variant: \"alternate\",\n      isAlternateRight: false,\n      class:\n        \"top-2 -right-[calc(var(--timeline-connector-thickness)/2)] h-full w-[var(--timeline-connector-thickness)]\",\n    },\n    {\n      orientation: \"vertical\",\n      variant: \"alternate\",\n      isAlternateRight: true,\n      class:\n        \"top-2 -left-[calc(var(--timeline-connector-thickness)/2)] h-full w-[var(--timeline-connector-thickness)]\",\n    },\n    {\n      orientation: \"horizontal\",\n      variant: \"alternate\",\n      class:\n        \"top-[calc(var(--timeline-dot-size)/2-var(--timeline-connector-thickness)/2)] left-3 row-start-2 h-[var(--timeline-connector-thickness)] w-[calc(100%+0.5rem)]\",\n    },\n  ],\n  defaultVariants: {\n    isCompleted: false,\n    orientation: \"vertical\",\n    variant: \"default\",\n    isAlternateRight: false,\n  },\n});\n\ninterface TimelineConnectorProps extends DivProps {\n  forceMount?: boolean;\n}\n\nfunction TimelineConnector(props: TimelineConnectorProps) {\n  const { asChild, forceMount, className, ...connectorProps } = props;\n\n  const { orientation, variant, activeIndex } =\n    useTimelineContext(CONNECTOR_NAME);\n  const { id, status, isAlternateRight } =\n    useTimelineItemContext(CONNECTOR_NAME);\n\n  const nextItemStatus = useStore((state) =>\n    state.getNextItemStatus(id, activeIndex),\n  );\n\n  const isLastItem = nextItemStatus === undefined;\n\n  if (!forceMount && isLastItem) return null;\n\n  const isConnectorCompleted =\n    nextItemStatus === \"completed\" || nextItemStatus === \"active\";\n\n  const ConnectorPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <ConnectorPrimitive\n      aria-hidden=\"true\"\n      data-slot=\"timeline-connector\"\n      data-completed={isConnectorCompleted ? \"\" : undefined}\n      data-status={status}\n      data-orientation={orientation}\n      {...connectorProps}\n      className={cn(\n        timelineConnectorVariants({\n          isCompleted: isConnectorCompleted,\n          orientation,\n          variant,\n          isAlternateRight,\n          className,\n        }),\n      )}\n    />\n  );\n}\n\nfunction TimelineHeader(props: DivProps) {\n  const { asChild, className, ...headerProps } = props;\n\n  const HeaderPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <HeaderPrimitive\n      data-slot=\"timeline-header\"\n      {...headerProps}\n      className={cn(\"flex flex-col gap-1\", className)}\n    />\n  );\n}\n\nfunction TimelineTitle(props: DivProps) {\n  const { asChild, className, ...titleProps } = props;\n\n  const TitlePrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <TitlePrimitive\n      data-slot=\"timeline-title\"\n      {...titleProps}\n      className={cn(\"leading-none font-semibold\", className)}\n    />\n  );\n}\n\nfunction TimelineDescription(props: DivProps) {\n  const { asChild, className, ...descriptionProps } = props;\n\n  const DescriptionPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <DescriptionPrimitive\n      data-slot=\"timeline-description\"\n      {...descriptionProps}\n      className={cn(\"text-sm text-muted-foreground\", className)}\n    />\n  );\n}\n\ninterface TimelineTimeProps extends React.ComponentProps<\"time\"> {\n  asChild?: boolean;\n}\n\nfunction TimelineTime(props: TimelineTimeProps) {\n  const { asChild, className, ...timeProps } = props;\n\n  const TimePrimitive = asChild ? SlotPrimitive.Slot : \"time\";\n\n  return (\n    <TimePrimitive\n      data-slot=\"timeline-time\"\n      {...timeProps}\n      className={cn(\"text-xs text-muted-foreground\", className)}\n    />\n  );\n}\n\nexport {\n  Timeline,\n  TimelineConnector,\n  TimelineContent,\n  TimelineDescription,\n  TimelineDot,\n  TimelineHeader,\n  TimelineItem,\n  type TimelineProps,\n  TimelineTime,\n  TimelineTitle,\n};\n",
      "target": ""
    },
    {
      "path": "lib/compose-refs.ts",
      "type": "registry:lib",
      "content": "/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx\n */\n\nimport * as React from \"react\";\n\ntype PossibleRef<T> = React.Ref<T> | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef<T>(ref: PossibleRef<T>, value: T) {\n  if (typeof ref === \"function\") {\n    return ref(value);\n  }\n\n  if (ref !== null && ref !== undefined) {\n    ref.current = value;\n  }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  return (node) => {\n    let hasCleanup = false;\n    const cleanups = refs.map((ref) => {\n      const cleanup = setRef(ref, node);\n      if (!hasCleanup && typeof cleanup === \"function\") {\n        hasCleanup = true;\n      }\n      return cleanup;\n    });\n\n    // React <19 will log an error to the console if a callback ref returns a\n    // value. We don't use ref cleanups internally so this will only happen if a\n    // user's ref callback returns a value, which we only expect if they are\n    // using the cleanup functionality added in React 19.\n    if (hasCleanup) {\n      return () => {\n        for (let i = 0; i < cleanups.length; i++) {\n          const cleanup = cleanups[i];\n          if (typeof cleanup === \"function\") {\n            cleanup();\n          } else {\n            setRef(refs[i], null);\n          }\n        }\n      };\n    }\n  };\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  // oxlint-disable-next-line react/exhaustive-deps -- we want to memoize by all values\n  return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { composeRefs, useComposedRefs };\n",
      "target": ""
    }
  ]
}