inline-citation

PreviousNext
Docs
takiui

Preview

Loading preview…
registry/new-york/ai-elements/inline-citation.tsx
"use client"

import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useState,
  type ComponentProps,
} from "react"
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"
import { Focusable } from "react-aria-components"

import { cn } from "@/lib/utils"
import { Badge } from "@/registry/new-york/ui/badge"
import {
  Carousel,
  CarouselContent,
  CarouselItem,
  type CarouselApi,
} from "@/registry/new-york/ui/carousel"
import { HoverCard, HoverCardContent } from "@/registry/new-york/ui/hover-card"

export type InlineCitationProps = ComponentProps<"span">

export const InlineCitation = ({
  className,
  ...props
}: InlineCitationProps) => (
  <span
    className={cn("group inline items-center gap-1", className)}
    {...props}
  />
)

export type InlineCitationTextProps = ComponentProps<"span">

export const InlineCitationText = ({
  className,
  ...props
}: InlineCitationTextProps) => (
  <span
    className={cn("group-hover:bg-accent transition-colors", className)}
    {...props}
  />
)

export type InlineCitationCardProps = ComponentProps<typeof HoverCard>

export const InlineCitationCard = (props: InlineCitationCardProps) => (
  <HoverCard closeDelay={0} delay={0} {...props} />
)

export type InlineCitationCardTriggerProps = ComponentProps<typeof Badge> & {
  sources: string[]
}

export const InlineCitationCardTrigger = ({
  sources,
  className,
  ...props
}: InlineCitationCardTriggerProps) => (
  <Focusable>
    <Badge
      role="button"
      className={cn("ml-1 rounded-full", className)}
      variant="secondary"
      {...props}
    >
      {sources.length ? (
        <>
          {new URL(sources[0]).hostname}{" "}
          {sources.length > 1 && `+${sources.length - 1}`}
        </>
      ) : (
        "unknown"
      )}
    </Badge>
  </Focusable>
)

export type InlineCitationCardBodyProps = ComponentProps<"div">

export const InlineCitationCardBody = ({
  className,
  ...props
}: InlineCitationCardBodyProps) => (
  <HoverCardContent className={cn("relative w-80 p-0", className)} {...props} />
)

const CarouselApiContext = createContext<CarouselApi | undefined>(undefined)

const useCarouselApi = () => {
  const context = useContext(CarouselApiContext)
  return context
}

export type InlineCitationCarouselProps = ComponentProps<typeof Carousel>

export const InlineCitationCarousel = ({
  className,
  children,
  ...props
}: InlineCitationCarouselProps) => {
  const [api, setApi] = useState<CarouselApi>()

  return (
    <CarouselApiContext.Provider value={api}>
      <Carousel className={cn("w-full", className)} setApi={setApi} {...props}>
        {children}
      </Carousel>
    </CarouselApiContext.Provider>
  )
}

export type InlineCitationCarouselContentProps = ComponentProps<"div">

export const InlineCitationCarouselContent = (
  props: InlineCitationCarouselContentProps
) => <CarouselContent {...props} />

export type InlineCitationCarouselItemProps = ComponentProps<"div">

export const InlineCitationCarouselItem = ({
  className,
  ...props
}: InlineCitationCarouselItemProps) => (
  <CarouselItem
    className={cn("w-full space-y-2 p-4 pl-8", className)}
    {...props}
  />
)

export type InlineCitationCarouselHeaderProps = ComponentProps<"div">

export const InlineCitationCarouselHeader = ({
  className,
  ...props
}: InlineCitationCarouselHeaderProps) => (
  <div
    className={cn(
      "bg-secondary flex items-center justify-between gap-2 rounded-t-md p-2",
      className
    )}
    {...props}
  />
)

export type InlineCitationCarouselIndexProps = ComponentProps<"div">

export const InlineCitationCarouselIndex = ({
  children,
  className,
  ...props
}: InlineCitationCarouselIndexProps) => {
  const api = useCarouselApi()
  const [current, setCurrent] = useState(0)
  const [count, setCount] = useState(0)

  useEffect(() => {
    if (!api) {
      return
    }

    setCount(api.scrollSnapList().length)
    setCurrent(api.selectedScrollSnap() + 1)

    api.on("select", () => {
      setCurrent(api.selectedScrollSnap() + 1)
    })
  }, [api])

  return (
    <div
      className={cn(
        "text-muted-foreground flex flex-1 items-center justify-end px-3 py-1 text-xs",
        className
      )}
      {...props}
    >
      {children ?? `${current}/${count}`}
    </div>
  )
}

export type InlineCitationCarouselPrevProps = ComponentProps<"button">

export const InlineCitationCarouselPrev = ({
  className,
  ...props
}: InlineCitationCarouselPrevProps) => {
  const api = useCarouselApi()

  const handleClick = useCallback(() => {
    if (api) {
      api.scrollPrev()
    }
  }, [api])

  return (
    <button
      aria-label="Previous"
      className={cn("shrink-0", className)}
      onClick={handleClick}
      type="button"
      {...props}
    >
      <ArrowLeftIcon className="text-muted-foreground size-4" />
    </button>
  )
}

export type InlineCitationCarouselNextProps = ComponentProps<"button">

export const InlineCitationCarouselNext = ({
  className,
  ...props
}: InlineCitationCarouselNextProps) => {
  const api = useCarouselApi()

  const handleClick = useCallback(() => {
    if (api) {
      api.scrollNext()
    }
  }, [api])

  return (
    <button
      aria-label="Next"
      className={cn("shrink-0", className)}
      onClick={handleClick}
      type="button"
      {...props}
    >
      <ArrowRightIcon className="text-muted-foreground size-4" />
    </button>
  )
}

export type InlineCitationSourceProps = ComponentProps<"div"> & {
  title?: string
  url?: string
  description?: string
}

export const InlineCitationSource = ({
  title,
  url,
  description,
  className,
  children,
  ...props
}: InlineCitationSourceProps) => (
  <div className={cn("space-y-1", className)} {...props}>
    {title && (
      <h4 className="truncate text-sm leading-tight font-medium">{title}</h4>
    )}
    {url && (
      <p className="text-muted-foreground truncate text-xs break-all">{url}</p>
    )}
    {description && (
      <p className="text-muted-foreground line-clamp-3 text-sm leading-relaxed">
        {description}
      </p>
    )}
    {children}
  </div>
)

export type InlineCitationQuoteProps = ComponentProps<"blockquote">

export const InlineCitationQuote = ({
  children,
  className,
  ...props
}: InlineCitationQuoteProps) => (
  <blockquote
    className={cn(
      "border-muted text-muted-foreground border-l-2 pl-3 text-sm italic",
      className
    )}
    {...props}
  >
    {children}
  </blockquote>
)

Installation

npx shadcn@latest add @taki/inline-citation

Usage

import { InlineCitation } from "@/components/ui/inline-citation"
<InlineCitation />