@alpic-ai/ui 0.0.0-dev.g23b48a1 → 0.0.0-dev.g23fdbf3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/dist/components/accordion-card.d.mts +5 -6
  2. package/dist/components/accordion.d.mts +5 -6
  3. package/dist/components/alert.d.mts +9 -11
  4. package/dist/components/area-chart.d.mts +62 -0
  5. package/dist/components/area-chart.mjs +269 -0
  6. package/dist/components/attachment-tile.d.mts +1 -3
  7. package/dist/components/avatar.d.mts +8 -10
  8. package/dist/components/badge.d.mts +2 -4
  9. package/dist/components/bar-chart.d.mts +48 -0
  10. package/dist/components/bar-chart.mjs +245 -0
  11. package/dist/components/bar-list.d.mts +28 -0
  12. package/dist/components/bar-list.mjs +98 -0
  13. package/dist/components/breadcrumb.d.mts +10 -11
  14. package/dist/components/button.d.mts +6 -8
  15. package/dist/components/card.d.mts +9 -10
  16. package/dist/components/chart-card.d.mts +25 -0
  17. package/dist/components/chart-card.mjs +48 -0
  18. package/dist/components/chart-container.d.mts +20 -0
  19. package/dist/components/chart-container.mjs +37 -0
  20. package/dist/components/chart-legend.d.mts +16 -0
  21. package/dist/components/chart-legend.mjs +26 -0
  22. package/dist/components/chart-tooltip.d.mts +33 -0
  23. package/dist/components/chart-tooltip.mjs +52 -0
  24. package/dist/components/checkbox.d.mts +2 -3
  25. package/dist/components/collapsible.d.mts +4 -5
  26. package/dist/components/combobox.d.mts +12 -11
  27. package/dist/components/combobox.mjs +7 -4
  28. package/dist/components/command.d.mts +9 -10
  29. package/dist/components/copyable.d.mts +2 -3
  30. package/dist/components/description-list.d.mts +5 -6
  31. package/dist/components/dialog.d.mts +15 -17
  32. package/dist/components/donut-chart.d.mts +46 -0
  33. package/dist/components/donut-chart.mjs +185 -0
  34. package/dist/components/dropdown-menu.d.mts +18 -20
  35. package/dist/components/form.d.mts +38 -21
  36. package/dist/components/form.mjs +6 -6
  37. package/dist/components/github-button.d.mts +1 -2
  38. package/dist/components/grid-fx.d.mts +13 -0
  39. package/dist/components/grid-fx.mjs +188 -0
  40. package/dist/components/heatmap-chart.d.mts +40 -0
  41. package/dist/components/heatmap-chart.mjs +198 -0
  42. package/dist/components/input-group.d.mts +5 -7
  43. package/dist/components/input.d.mts +4 -5
  44. package/dist/components/input.mjs +2 -2
  45. package/dist/components/label.d.mts +2 -3
  46. package/dist/components/line-chart.d.mts +55 -0
  47. package/dist/components/line-chart.mjs +211 -0
  48. package/dist/components/page-loader.d.mts +1 -3
  49. package/dist/components/pagination.d.mts +3 -4
  50. package/dist/components/popover.d.mts +5 -6
  51. package/dist/components/radio-group.d.mts +3 -4
  52. package/dist/components/scroll-area.d.mts +3 -4
  53. package/dist/components/select-trigger-variants.d.mts +1 -3
  54. package/dist/components/select.d.mts +9 -10
  55. package/dist/components/separator.d.mts +2 -3
  56. package/dist/components/sheet.d.mts +11 -12
  57. package/dist/components/shimmer-text.d.mts +2 -2
  58. package/dist/components/sidebar.d.mts +34 -36
  59. package/dist/components/sidebar.mjs +10 -10
  60. package/dist/components/skeleton.d.mts +2 -4
  61. package/dist/components/sonner.d.mts +5 -6
  62. package/dist/components/spinner.d.mts +3 -5
  63. package/dist/components/stat.d.mts +30 -0
  64. package/dist/components/stat.mjs +107 -0
  65. package/dist/components/status-dot.d.mts +2 -4
  66. package/dist/components/switch.d.mts +2 -3
  67. package/dist/components/table.d.mts +10 -11
  68. package/dist/components/tabs.d.mts +12 -14
  69. package/dist/components/tag.d.mts +3 -5
  70. package/dist/components/task-progress.d.mts +1 -3
  71. package/dist/components/textarea.d.mts +3 -4
  72. package/dist/components/textarea.mjs +2 -2
  73. package/dist/components/toggle-group.d.mts +4 -6
  74. package/dist/components/toggle-group.mjs +3 -3
  75. package/dist/components/tooltip-icon-button.d.mts +1 -2
  76. package/dist/components/tooltip.d.mts +5 -6
  77. package/dist/components/typography.d.mts +4 -5
  78. package/dist/components/wizard.d.mts +4 -5
  79. package/dist/hooks/use-chart-theme.d.mts +18 -0
  80. package/dist/hooks/use-chart-theme.mjs +57 -0
  81. package/dist/hooks/use-mobile.mjs +3 -3
  82. package/dist/hooks/use-reduced-motion.d.mts +4 -0
  83. package/dist/hooks/use-reduced-motion.mjs +16 -0
  84. package/dist/lib/chart-palette.d.mts +4 -0
  85. package/dist/lib/chart-palette.mjs +95 -0
  86. package/dist/lib/chart.d.mts +14 -0
  87. package/dist/lib/chart.mjs +27 -0
  88. package/package.json +30 -29
  89. package/src/components/area-chart.tsx +339 -0
  90. package/src/components/bar-chart.tsx +300 -0
  91. package/src/components/bar-list.tsx +150 -0
  92. package/src/components/chart-card.tsx +63 -0
  93. package/src/components/chart-container.tsx +49 -0
  94. package/src/components/chart-legend.tsx +41 -0
  95. package/src/components/chart-tooltip.tsx +93 -0
  96. package/src/components/combobox.tsx +9 -2
  97. package/src/components/donut-chart.tsx +217 -0
  98. package/src/components/grid-fx.tsx +238 -0
  99. package/src/components/heatmap-chart.tsx +287 -0
  100. package/src/components/line-chart.tsx +264 -0
  101. package/src/components/stat.tsx +109 -0
  102. package/src/hooks/use-chart-theme.ts +75 -0
  103. package/src/hooks/use-reduced-motion.ts +17 -0
  104. package/src/lib/chart-palette.ts +110 -0
  105. package/src/lib/chart.ts +56 -0
  106. package/src/stories/area-chart.stories.tsx +200 -0
  107. package/src/stories/bar-chart.stories.tsx +169 -0
  108. package/src/stories/bar-list.stories.tsx +85 -0
  109. package/src/stories/donut-chart.stories.tsx +112 -0
  110. package/src/stories/heatmap-chart.stories.tsx +107 -0
  111. package/src/stories/line-chart.stories.tsx +146 -0
  112. package/src/stories/stat.stories.tsx +64 -0
  113. package/src/styles/tokens.css +63 -0
@@ -21,6 +21,8 @@ interface ComboboxContextValue {
21
21
  isSelected: (itemValue: string) => boolean;
22
22
  open: boolean;
23
23
  onOpenChange: (open: boolean) => void;
24
+ /** Resolves a selected value to a display label (e.g. for multi-select tags). Defaults to the value. */
25
+ getOptionLabel?: (value: string) => string;
24
26
  }
25
27
 
26
28
  const ComboboxContext = createContext<ComboboxContextValue | null>(null);
@@ -40,6 +42,8 @@ interface ComboboxBaseProps {
40
42
  open?: boolean;
41
43
  defaultOpen?: boolean;
42
44
  onOpenChange?: (open: boolean) => void;
45
+ /** Resolves a selected value to a display label (e.g. for multi-select tags). Defaults to the value. */
46
+ getOptionLabel?: (value: string) => string;
43
47
  }
44
48
 
45
49
  interface ComboboxSingleProps extends ComboboxBaseProps {
@@ -65,6 +69,7 @@ function Combobox(props: ComboboxProps) {
65
69
  open: controlledOpen,
66
70
  defaultOpen = false,
67
71
  onOpenChange: controlledOnOpenChange,
72
+ getOptionLabel,
68
73
  } = props;
69
74
 
70
75
  // Single mode state
@@ -175,8 +180,9 @@ function Combobox(props: ComboboxProps) {
175
180
  isSelected,
176
181
  open,
177
182
  onOpenChange,
183
+ getOptionLabel,
178
184
  }),
179
- [multiple, singleValue, multiValues, onSelect, onDeselect, isSelected, open, onOpenChange],
185
+ [multiple, singleValue, multiValues, onSelect, onDeselect, isSelected, open, onOpenChange, getOptionLabel],
180
186
  );
181
187
 
182
188
  return (
@@ -229,6 +235,7 @@ function ComboboxTrigger({ className, size, placeholder, children, ...props }: C
229
235
  /* ── Tags (internal, for multi-select trigger) ────────────────────────────── */
230
236
 
231
237
  function ComboboxTags({ values, onDeselect }: { values: string[]; onDeselect: (value: string) => void }) {
238
+ const { getOptionLabel } = useComboboxContext();
232
239
  return (
233
240
  <>
234
241
  {values.map((tagValue) => (
@@ -237,7 +244,7 @@ function ComboboxTags({ values, onDeselect }: { values: string[]; onDeselect: (v
237
244
  onClick={(event) => event.stopPropagation()}
238
245
  onDismiss={() => onDeselect(tagValue)}
239
246
  >
240
- {tagValue}
247
+ {getOptionLabel ? getOptionLabel(tagValue) : tagValue}
241
248
  </TagDismissible>
242
249
  ))}
243
250
  </>
@@ -0,0 +1,217 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Cell, Pie, PieChart as RechartsPieChart, ResponsiveContainer } from "recharts";
5
+
6
+ import { useReducedMotion } from "../hooks/use-reduced-motion";
7
+ import { formatShare } from "../lib/chart";
8
+ import type { ChartPaletteName } from "../lib/chart-palette";
9
+ import { paletteColor } from "../lib/chart-palette";
10
+ import { cn } from "../lib/cn";
11
+ import { useChartContext } from "./chart-container";
12
+
13
+ const GEOMETRY = {
14
+ donut: { inner: "64%", outer: "92%" },
15
+ ring: { inner: "78%", outer: "92%" },
16
+ } as const;
17
+
18
+ export interface DonutChartProps {
19
+ data: ReadonlyArray<Record<string, string | number | null | undefined>>;
20
+ index: string;
21
+ dataKey?: string;
22
+ variant?: keyof typeof GEOMETRY;
23
+ legend?: boolean;
24
+ paddingAngle?: number;
25
+ height?: number;
26
+ palette?: ChartPaletteName;
27
+ centerLabel?: string;
28
+ loading?: boolean;
29
+ valueFormatter?: (value: number) => string;
30
+ labelFormatter?: (label: string | number) => string;
31
+ className?: string;
32
+ }
33
+
34
+ function DonutChart({
35
+ data,
36
+ index,
37
+ dataKey = "value",
38
+ variant = "donut",
39
+ legend = false,
40
+ paddingAngle = 1,
41
+ height = 220,
42
+ palette,
43
+ centerLabel = "total",
44
+ loading = false,
45
+ valueFormatter = (value) => value.toLocaleString("en-US"),
46
+ labelFormatter,
47
+ className,
48
+ }: DonutChartProps) {
49
+ const { palette: paletteColors, theme } = useChartContext(palette);
50
+ const reactId = React.useId().replace(/:/g, "");
51
+ const reducedMotion = useReducedMotion();
52
+ const animated = !reducedMotion;
53
+ const [active, setActive] = React.useState<number | null>(null);
54
+ const rowRefs = React.useRef<Array<HTMLDivElement | null>>([]);
55
+
56
+ const slices = React.useMemo(() => {
57
+ const mapped = data.map((row) => ({
58
+ name: String(row[index] ?? ""),
59
+ value: Number(row[dataKey]) || 0,
60
+ }));
61
+ mapped.sort((lower, upper) => upper.value - lower.value);
62
+ return mapped.map((slice, rank) => ({ ...slice, color: paletteColor(paletteColors, rank) }));
63
+ }, [data, index, dataKey, paletteColors]);
64
+
65
+ const total = slices.reduce((sum, slice) => sum + slice.value, 0);
66
+ const maxValue = slices.reduce((max, slice) => (slice.value > max ? slice.value : max), 0);
67
+ const geometry = GEOMETRY[variant] ?? GEOMETRY.donut;
68
+ const formatName = (name: string) => (labelFormatter ? labelFormatter(name) : name);
69
+
70
+ const isEmpty = slices.length === 0 || total <= 0;
71
+
72
+ const activeSlice = active !== null ? slices[active] : undefined;
73
+ const centerTitle = activeSlice ? formatName(activeSlice.name) : centerLabel;
74
+ const centerValue = valueFormatter(activeSlice ? activeSlice.value : total);
75
+
76
+ React.useEffect(() => {
77
+ if (active !== null) {
78
+ rowRefs.current[active]?.scrollIntoView({ block: "nearest" });
79
+ }
80
+ }, [active]);
81
+
82
+ return (
83
+ <div data-slot="donut-chart" className={cn("@container flex w-full flex-col gap-3", className)}>
84
+ {loading ? (
85
+ <div
86
+ className="flex items-center justify-center gap-2.5 font-mono text-quaternary-foreground text-text-xs"
87
+ style={{ height }}
88
+ >
89
+ <span className="size-4 animate-spin rounded-full border-2 border-border border-t-foreground" />
90
+ loading…
91
+ </div>
92
+ ) : isEmpty ? (
93
+ <div
94
+ className="flex items-center justify-center rounded-lg border border-border border-dashed font-mono text-quaternary-foreground text-text-xs"
95
+ style={{ height }}
96
+ >
97
+ no data in range
98
+ </div>
99
+ ) : (
100
+ <div className="flex flex-col items-center gap-5 @md:flex-row">
101
+ <div className="relative shrink-0" style={{ width: height, height }}>
102
+ <ResponsiveContainer width="100%" height="100%">
103
+ <RechartsPieChart>
104
+ <defs>
105
+ {slices.map((slice, slot) => (
106
+ <linearGradient key={slice.name} id={`donut-${reactId}-${slot}`} x1="0" y1="0" x2="0" y2="1">
107
+ <stop offset="0%" stopColor={slice.color} stopOpacity={1} />
108
+ <stop offset="100%" stopColor={slice.color} stopOpacity={0.7} />
109
+ </linearGradient>
110
+ ))}
111
+ </defs>
112
+ <Pie
113
+ data={slices}
114
+ dataKey="value"
115
+ nameKey="name"
116
+ innerRadius={geometry.inner}
117
+ outerRadius={geometry.outer}
118
+ paddingAngle={paddingAngle}
119
+ startAngle={90}
120
+ endAngle={-270}
121
+ cornerRadius={2}
122
+ stroke={theme.card}
123
+ strokeWidth={1.5}
124
+ isAnimationActive={animated}
125
+ animationDuration={650}
126
+ animationEasing="ease-out"
127
+ onMouseEnter={(_entry, sliceIndex) => setActive(sliceIndex)}
128
+ onMouseLeave={() => setActive(null)}
129
+ >
130
+ {slices.map((slice, slot) => (
131
+ <Cell
132
+ key={slice.name}
133
+ className="motion-safe:[transition:fill-opacity_180ms_ease-out]"
134
+ fill={`url(#donut-${reactId}-${slot})`}
135
+ fillOpacity={active === null || active === slot ? 1 : 0.55}
136
+ stroke={theme.card}
137
+ strokeWidth={1.5}
138
+ />
139
+ ))}
140
+ </Pie>
141
+ </RechartsPieChart>
142
+ </ResponsiveContainer>
143
+
144
+ <div className="pointer-events-none absolute inset-0 flex flex-col items-center justify-center gap-1 text-center">
145
+ <span className="max-w-[72%] truncate font-mono text-[10px] text-quaternary-foreground uppercase tracking-[0.18em]">
146
+ {centerTitle}
147
+ </span>
148
+ <span className="font-mono font-semibold text-[28px] text-foreground leading-none tabular-nums">
149
+ {centerValue}
150
+ </span>
151
+ {activeSlice && (
152
+ <span className="font-mono font-medium text-[11px] tabular-nums" style={{ color: activeSlice.color }}>
153
+ {formatShare(activeSlice.value / total)}
154
+ </span>
155
+ )}
156
+ </div>
157
+ </div>
158
+
159
+ {legend && (
160
+ <div
161
+ data-slot="donut-readout"
162
+ className="flex min-w-0 flex-1 flex-col overflow-y-auto pr-1"
163
+ style={{ maxHeight: height }}
164
+ >
165
+ {slices.map((slice, slot) => (
166
+ // biome-ignore lint/a11y/noStaticElementInteractions: decorative hover-sync; all values are visible without it
167
+ <div
168
+ key={slice.name}
169
+ ref={(node) => {
170
+ rowRefs.current[slot] = node;
171
+ }}
172
+ onMouseEnter={() => setActive(slot)}
173
+ onMouseLeave={() => setActive(null)}
174
+ className={cn(
175
+ "flex flex-col gap-1.5 border-border/40 border-b px-2 py-2 text-text-xs last:border-b-0 motion-safe:transition-colors",
176
+ active === slot ? "bg-muted/50" : "bg-transparent",
177
+ )}
178
+ >
179
+ <div className="flex items-center justify-between gap-3">
180
+ <span className="inline-flex min-w-0 items-center gap-2 text-muted-foreground">
181
+ <span
182
+ aria-hidden
183
+ className="h-2 w-2.5 shrink-0 rounded-[3px]"
184
+ style={{ background: slice.color }}
185
+ />
186
+ <span className="truncate">{formatName(slice.name)}</span>
187
+ </span>
188
+ <span className="flex shrink-0 items-center gap-3 font-mono tabular-nums">
189
+ <span className="min-w-[3.5rem] text-right font-semibold text-foreground">
190
+ {valueFormatter(slice.value)}
191
+ </span>
192
+ <span className="w-10 text-right text-quaternary-foreground">
193
+ {formatShare(slice.value / total)}
194
+ </span>
195
+ </span>
196
+ </div>
197
+ <span aria-hidden className="relative block h-[2px] w-full overflow-hidden rounded-full bg-border/40">
198
+ <span
199
+ className="absolute inset-y-0 left-0 rounded-full motion-safe:transition-[width] motion-safe:duration-500"
200
+ style={{
201
+ width: `${maxValue > 0 ? (slice.value / maxValue) * 100 : 0}%`,
202
+ background: slice.color,
203
+ opacity: active === null || active === slot ? 1 : 0.45,
204
+ }}
205
+ />
206
+ </span>
207
+ </div>
208
+ ))}
209
+ </div>
210
+ )}
211
+ </div>
212
+ )}
213
+ </div>
214
+ );
215
+ }
216
+
217
+ export { DonutChart };
@@ -0,0 +1,238 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ import { useReducedMotion } from "../hooks/use-reduced-motion";
6
+ import { cn } from "../lib/cn";
7
+
8
+ const CELL_SIZE = 46;
9
+ const TTL_MIN = 42;
10
+ const TTL_MAX = 78;
11
+ const SPAWN_MIN = 180;
12
+ const SPAWN_MAX = 480;
13
+
14
+ interface GlitchLine {
15
+ horiz: boolean;
16
+ at: number;
17
+ life: number;
18
+ ttl: number;
19
+ color: string;
20
+ colorHi: string;
21
+ }
22
+
23
+ const rand = (min: number, max: number) => min + Math.random() * (max - min);
24
+
25
+ function resolveColors(element: HTMLElement) {
26
+ const styles = getComputedStyle(element);
27
+ return {
28
+ color: styles.getPropertyValue("--color-primary").trim() || "#e90060" /* --color-primary */,
29
+ colorHi: styles.getPropertyValue("--color-primary-hover").trim() || "#f22b79" /* --color-primary-hover */,
30
+ };
31
+ }
32
+
33
+ function strokeFull(ctx: CanvasRenderingContext2D, horiz: boolean, at: number, width: number, height: number) {
34
+ ctx.beginPath();
35
+ if (horiz) {
36
+ ctx.moveTo(0, at);
37
+ ctx.lineTo(width, at);
38
+ } else {
39
+ ctx.moveTo(at, 0);
40
+ ctx.lineTo(at, height);
41
+ }
42
+ ctx.stroke();
43
+ }
44
+
45
+ function drawGlitchLine(ctx: CanvasRenderingContext2D, line: GlitchLine, width: number, height: number) {
46
+ const { horiz, at, color, colorHi } = line;
47
+ const span = horiz ? width : height;
48
+ const progress = line.life / line.ttl;
49
+ const envelope = progress < 0.08 ? progress / 0.08 : 1 - (progress - 0.08) / 0.92;
50
+ const base = Math.max(0, envelope);
51
+
52
+ ctx.lineCap = "round";
53
+
54
+ const ghosts = [
55
+ { offset: 0, alpha: 0.85, blur: 12 },
56
+ { offset: rand(-3, 3), alpha: 0.35, blur: 0 },
57
+ { offset: rand(-7, 7), alpha: 0.2, blur: 0 },
58
+ ];
59
+ for (const ghost of ghosts) {
60
+ ctx.globalAlpha = base * ghost.alpha * (Math.random() < 0.1 ? 0.3 : 1);
61
+ ctx.strokeStyle = color;
62
+ ctx.lineWidth = 1.5;
63
+ ctx.shadowBlur = ghost.blur;
64
+ ctx.shadowColor = color;
65
+ strokeFull(ctx, horiz, at + ghost.offset, width, height);
66
+ }
67
+
68
+ if (Math.random() < 0.5) {
69
+ ctx.globalAlpha = base * 0.6;
70
+ ctx.lineWidth = 2;
71
+ ctx.shadowBlur = 0;
72
+ ctx.strokeStyle = colorHi;
73
+ for (let segment = 0; segment < 3; segment++) {
74
+ const start = rand(0, span * 0.85);
75
+ const end = start + rand(20, 80);
76
+ const jitter = rand(-4, 4);
77
+ ctx.beginPath();
78
+ if (horiz) {
79
+ ctx.moveTo(start, at + jitter);
80
+ ctx.lineTo(end, at + jitter);
81
+ } else {
82
+ ctx.moveTo(at + jitter, start);
83
+ ctx.lineTo(at + jitter, end);
84
+ }
85
+ ctx.stroke();
86
+ }
87
+ }
88
+ }
89
+
90
+ function GridFx({
91
+ className,
92
+ cellSize = CELL_SIZE,
93
+ style,
94
+ ...props
95
+ }: React.ComponentProps<"canvas"> & { cellSize?: number }) {
96
+ const reduced = useReducedMotion();
97
+ const canvasRef = React.useRef<HTMLCanvasElement>(null);
98
+
99
+ React.useEffect(() => {
100
+ if (reduced) {
101
+ return;
102
+ }
103
+ const canvas = canvasRef.current;
104
+ const parent = canvas?.parentElement;
105
+ const ctx = canvas?.getContext("2d");
106
+ if (!canvas || !parent || !ctx) {
107
+ return;
108
+ }
109
+
110
+ let width = 0;
111
+ let height = 0;
112
+ let dpr = 1;
113
+ let frame = 0;
114
+ let nextIn = rand(SPAWN_MIN, SPAWN_MAX);
115
+ let onScreen = true;
116
+ const lines: GlitchLine[] = [];
117
+
118
+ const resize = () => {
119
+ dpr = Math.min(window.devicePixelRatio || 1, 2);
120
+ width = parent.clientWidth;
121
+ height = parent.clientHeight;
122
+ canvas.width = width * dpr;
123
+ canvas.height = height * dpr;
124
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
125
+ };
126
+
127
+ const spawn = () => {
128
+ const horiz = Math.random() < 0.5;
129
+ const tracks = Math.ceil((horiz ? height : width) / cellSize);
130
+ const { color, colorHi } = resolveColors(canvas);
131
+ lines.push({
132
+ horiz,
133
+ at: Math.floor(Math.random() * tracks) * cellSize + 0.5,
134
+ life: 0,
135
+ ttl: rand(TTL_MIN, TTL_MAX),
136
+ color,
137
+ colorHi,
138
+ });
139
+ };
140
+
141
+ const tick = () => {
142
+ ctx.globalAlpha = 1;
143
+ ctx.shadowBlur = 0;
144
+ ctx.clearRect(0, 0, width, height);
145
+
146
+ if (--nextIn <= 0) {
147
+ spawn();
148
+ nextIn = rand(SPAWN_MIN, SPAWN_MAX);
149
+ }
150
+ for (let index = lines.length - 1; index >= 0; index--) {
151
+ const line = lines[index];
152
+ if (!line) {
153
+ continue;
154
+ }
155
+ line.life++;
156
+ drawGlitchLine(ctx, line, width, height);
157
+ if (line.life >= line.ttl) {
158
+ lines.splice(index, 1);
159
+ }
160
+ }
161
+
162
+ ctx.globalAlpha = 1;
163
+ ctx.shadowBlur = 0;
164
+ frame = requestAnimationFrame(tick);
165
+ };
166
+
167
+ const running = () => onScreen && document.visibilityState === "visible";
168
+ const start = () => {
169
+ if (!frame && running()) {
170
+ frame = requestAnimationFrame(tick);
171
+ }
172
+ };
173
+ const stop = () => {
174
+ if (frame) {
175
+ cancelAnimationFrame(frame);
176
+ }
177
+ frame = 0;
178
+ ctx.clearRect(0, 0, width, height);
179
+ };
180
+
181
+ resize();
182
+ start();
183
+
184
+ const resizeObserver = new ResizeObserver(() => resize());
185
+ resizeObserver.observe(parent);
186
+
187
+ const intersectionObserver = new IntersectionObserver(([entry]) => {
188
+ if (!entry) {
189
+ return;
190
+ }
191
+ onScreen = entry.isIntersecting;
192
+ if (onScreen) {
193
+ start();
194
+ } else {
195
+ stop();
196
+ }
197
+ });
198
+ intersectionObserver.observe(canvas);
199
+
200
+ const onVisibility = () => {
201
+ if (running()) {
202
+ start();
203
+ } else {
204
+ stop();
205
+ }
206
+ };
207
+ document.addEventListener("visibilitychange", onVisibility);
208
+
209
+ return () => {
210
+ stop();
211
+ resizeObserver.disconnect();
212
+ intersectionObserver.disconnect();
213
+ document.removeEventListener("visibilitychange", onVisibility);
214
+ };
215
+ }, [reduced, cellSize]);
216
+
217
+ if (reduced) {
218
+ return null;
219
+ }
220
+
221
+ return (
222
+ <canvas
223
+ ref={canvasRef}
224
+ aria-hidden
225
+ data-slot="grid-fx"
226
+ className={cn("pointer-events-none absolute inset-0 h-full w-full", className)}
227
+ style={{
228
+ zIndex: -1,
229
+ WebkitMaskImage: "radial-gradient(120% 95% at 50% 0%, #000 30%, transparent 100%)",
230
+ maskImage: "radial-gradient(120% 95% at 50% 0%, #000 30%, transparent 100%)",
231
+ ...style,
232
+ }}
233
+ {...props}
234
+ />
235
+ );
236
+ }
237
+
238
+ export { GridFx };