Sunflower UI
Search
Copy Code
Light Mode

Soufrière Hills had been quiet for three centuries when it woke in July 1995 and began burying Plymouth, the capital of Montserrat, in ash.

WWikipedia


Pyroclastic flows overran the airport two years later, and two thirds of the island left for good.
...BBC +1

inline-citation.tsx
styles.css
prompt.md
Collapse
Reset
Copy Code
Download
Run
1
"use client";
2
3
import React, { useState, useRef } from "react";
4
import { ArrowLeft, ArrowRight } from "lucide-react";
5
import { motion, AnimatePresence } from "framer-motion";
6
import { cn } from "@/lib/utils";
7
8
export interface Citation {
9
id: string;
10
source: string;
11
icon: React.ReactNode;
12
title: string;
13
description: string;
14
date: string;
15
}
16
17
export interface InlineCitationProps {
18
citations: Citation[];
19
}
20
21
export function InlineCitation({ citations }: InlineCitationProps) {
22
const [isOpen, setIsOpen] = useState(false);
23
const [currentIndex, setCurrentIndex] = useState(0);
24
const [direction, setDirection] = useState(1);
25
const timeoutRef = useRef<NodeJS.Timeout>(undefined);
26
27
const activeCitation = citations[currentIndex];
28
const isMulti = citations.length > 1;
29
30
const handleMouseEnter = () => {
31
if (timeoutRef.current) clearTimeout(timeoutRef.current);
32
setIsOpen(true);
33
};
34
35
const handleMouseLeave = () => {
36
timeoutRef.current = setTimeout(() => {
37
setIsOpen(false);
38
// Reset after a delay so it doesn't jump while animating out
39
setTimeout(() => {
40
setCurrentIndex(0);
41
setDirection(1);
42
}, 300);
43
}, 150);
44
};
45
46
const next = (e: React.MouseEvent) => {
47
e.stopPropagation();
48
if (currentIndex < citations.length - 1) {
49
setDirection(1);
50
setCurrentIndex(prev => prev + 1);
51
}
52
};
53
54
const prev = (e: React.MouseEvent) => {
55
e.stopPropagation();
56
if (currentIndex > 0) {
57
setDirection(-1);
58
setCurrentIndex(prev => prev - 1);
59
}
60
};
61
62
return (
63
<div
64
className="relative inline-block"
65
onMouseEnter={handleMouseEnter}
66
onMouseLeave={handleMouseLeave}
67
>
68
<span
69
className={cn(
70
"inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-[11.5px] font-matter font-medium align-middle transition-all duration-200 cursor-pointer border shadow-sm",
71
isOpen
72
? "bg-zinc-900 text-white border-zinc-900 dark:bg-white dark:text-black dark:border-white shadow-md scale-[1.02]"
73
: "bg-zinc-100 text-zinc-700 border-zinc-200/80 hover:bg-zinc-200/80 dark:bg-gradient-to-b dark:from-[#2C2C2E] dark:to-[#1C1C1E] dark:text-[#F5F5F7] dark:border-white/10 dark:hover:border-white/25"
74
)}
75
>
76
<span
77
className={cn(
78
"flex items-center justify-center w-3.5 h-3.5 rounded-full transition-colors",
79
isOpen
80
? "bg-white text-zinc-900 dark:bg-black dark:text-white"
81
: "bg-zinc-900 text-white dark:bg-white dark:text-black"
82
)}
83
>
84
{citations[0].icon}
85
</span>
86
{citations[0].source} {isMulti && `+${citations.length - 1}`}
87
</span>
88
89
<AnimatePresence>
90
{isOpen && (
91
<motion.div
92
initial={{ opacity: 0, y: 8, scale: 0.96 }}
93
animate={{ opacity: 1, y: 0, scale: 1 }}
94
exit={{ opacity: 0, y: 4, scale: 0.96 }}
95
transition={{ duration: 0.15, ease: "easeOut" }}
96
className="absolute z-50 top-full left-1/2 -translate-x-1/2 mt-2 w-[320px] rounded-2xl bg-white dark:bg-gradient-to-b dark:from-[#1C1C1E] dark:to-[#0A0A0C] border border-zinc-200/80 dark:border-white/10 dark:border-t-white/20 shadow-2xl dark:shadow-[0_20px_50px_rgba(0,0,0,0.8),inset_0_1px_0_rgba(255,255,255,0.12)] backdrop-blur-xl overflow-hidden text-left"
97
>
98
<div className="p-4">
99
{isMulti && (
100
<div className="flex items-center justify-between mb-4">
101
<div className="flex items-center gap-1.5 -ml-1">
102
<button
103
onClick={prev}
104
disabled={currentIndex === 0}
105
className="p-1 rounded-md text-zinc-400 hover:text-zinc-800 hover:bg-zinc-100 disabled:opacity-30 disabled:hover:bg-transparent dark:text-[#86868B] dark:hover:text-[#F5F5F7] dark:hover:bg-white/10 transition-colors"
106
>
107
<ArrowLeft className="w-4 h-4" />
108
</button>
109
<button
110
onClick={next}
111
disabled={currentIndex === citations.length - 1}
112
className="p-1 rounded-md text-zinc-400 hover:text-zinc-800 hover:bg-zinc-100 disabled:opacity-30 disabled:hover:bg-transparent dark:text-[#86868B] dark:hover:text-[#F5F5F7] dark:hover:bg-white/10 transition-colors"
113
>
114
<ArrowRight className="w-4 h-4" />
115
</button>
116
</div>
117
<div className="text-[11px] font-mono text-zinc-400 dark:text-[#86868B]">
118
{currentIndex + 1} / {citations.length}
119
</div>
120
</div>
121
)}
122
123
<div className="overflow-hidden relative">
124
<AnimatePresence mode="wait" custom={direction}>
125
<motion.div
126
key={currentIndex}
127
custom={direction}
128
variants={{
129
enter: (d: number) => ({ opacity: 0, x: d * 10 }),
130
center: { opacity: 1, x: 0 },
131
exit: (d: number) => ({ opacity: 0, x: -d * 10 })
132
}}
133
initial="enter"
134
animate="center"
135
exit="exit"
136
transition={{ duration: 0.2, ease: "easeInOut" }}
137
className="flex flex-col"
138
>
139
<div className="flex items-center gap-2 text-xs font-matter font-medium text-zinc-600 dark:text-[#86868B]">
140
<span className="flex items-center justify-center w-[18px] h-[18px] rounded-full bg-zinc-100 dark:bg-white/10 text-zinc-900 dark:text-[#F5F5F7]">
141
{activeCitation.icon}
142
</span>
143
{activeCitation.source}
144
</div>
145
146
<h4 className="mt-3 text-[15px] font-matter font-medium text-zinc-900 dark:text-[#F5F5F7] leading-snug">
147
{activeCitation.title}
148
</h4>
149
150
<p className="mt-2 text-[13px] font-matter text-zinc-600 dark:text-[#86868B] leading-relaxed">
151
{activeCitation.description}
152
</p>
153
154
<div className="mt-4 text-[11px] font-mono text-zinc-400 dark:text-[#86868B]">
155
{activeCitation.date}
156
</div>
157
</motion.div>
158
</AnimatePresence>
159
</div>
160
</div>
161
</motion.div>
162
)}
163
</AnimatePresence>
164
</div>
165
);
166
}
167