diff --git a/package-lock.json b/package-lock.json index 5a0f24a..c286669 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,9 @@ "dependencies": { "@hookform/resolvers": "^5.2.2", "@opennextjs/cloudflare": "^1.10.1", + "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-hover-card": "^1.1.15", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-navigation-menu": "^1.2.14", "@radix-ui/react-progress": "^1.1.7", @@ -19,6 +21,7 @@ "@radix-ui/react-slot": "^1.2.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "framer-motion": "^12.23.24", "lucide-react": "^0.545.0", "next": "15.5.4", "open-next": "^3.1.3", @@ -6009,6 +6012,37 @@ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "license": "MIT" }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", @@ -6032,6 +6066,36 @@ } } }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collection": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", @@ -6206,6 +6270,37 @@ } } }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", + "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-id": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", @@ -10507,6 +10602,33 @@ "node": ">= 0.6" } }, + "node_modules/framer-motion": { + "version": "12.23.24", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz", + "integrity": "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.23", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", @@ -12605,6 +12727,21 @@ "obliterator": "^1.6.1" } }, + "node_modules/motion-dom": { + "version": "12.23.23", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", + "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/package.json b/package.json index 8c1c92e..e1305c2 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,9 @@ "dependencies": { "@hookform/resolvers": "^5.2.2", "@opennextjs/cloudflare": "^1.10.1", + "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-hover-card": "^1.1.15", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-navigation-menu": "^1.2.14", "@radix-ui/react-progress": "^1.1.7", @@ -22,6 +24,7 @@ "@radix-ui/react-slot": "^1.2.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "framer-motion": "^12.23.24", "lucide-react": "^0.545.0", "next": "15.5.4", "open-next": "^3.1.3", diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..9363a4f Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 0000000..fba1fe7 Binary files /dev/null and b/public/favicon.png differ diff --git a/public/logo apr transparent.png b/public/logo apr transparent.png deleted file mode 100644 index 6da91a5..0000000 Binary files a/public/logo apr transparent.png and /dev/null differ diff --git a/src/app/globals.css b/src/app/globals.css index 46d9fd3..4919797 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -59,6 +59,12 @@ --font-sans: var(--font-geist-sans); --font-mono: var(--font-geist-mono); --font-brand: var(--font-bebas); + --font-orbitron: var(--font-orbitron); + --font-inter: var(--font-inter); + --font-jetbrains-mono: var(--font-jetbrains-mono); + --font-space-mono: var(--font-space-mono); + --font-rajdhani: var(--font-rajdhani); + --font-exo-2: var(--font-exo-2); --color-background: hsl(var(--background)); --color-foreground: hsl(var(--foreground)); --color-border: hsl(var(--border)); @@ -67,7 +73,7 @@ body { background: hsl(var(--background)); color: hsl(var(--foreground)); - font-family: var(--font-sans), Arial, Helvetica, sans-serif; + font-family: var(--font-jetbrains-mono), Arial, Helvetica, sans-serif; } .font-brand { @@ -76,3 +82,87 @@ body { letter-spacing: 0.02em; text-transform: uppercase; } + +.font-orbitron { + font-family: var(--font-orbitron), var(--font-sans), Arial, Helvetica, sans-serif; + font-weight: 700; + letter-spacing: 0.05em; +} + +.font-inter { + font-family: var(--font-inter), Arial, Helvetica, sans-serif; +} + +.font-terminal { + font-family: var(--font-jetbrains-mono), monospace; + font-weight: 700; + letter-spacing: 0.02em; + text-transform: uppercase; +} + +.font-space-mono { + font-family: var(--font-space-mono), monospace; + font-weight: 700; + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.font-rajdhani { + font-family: var(--font-rajdhani), sans-serif; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.font-exo-2 { + font-family: var(--font-exo-2), sans-serif; + font-weight: 900; + letter-spacing: 0.05em; + text-transform: uppercase; +} + +/* Dot effect for black text - simplified approach */ +.text-dots { + position: relative; + color: #000000; + background-image: + radial-gradient(circle, #000000 1px, transparent 1px); + background-size: 3px 3px; + background-position: 0 0; + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; +} + +/* Custom dotted border with solid corner dots */ +.dotted-border-corners { + position: relative; +} + +.dotted-border-corners::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border: 1px dotted white; + pointer-events: none; +} + +.dotted-border-corners::after { + content: ''; + position: absolute; + top: -2px; + left: -2px; + right: -2px; + bottom: -2px; + background: + /* Corner dots */ + radial-gradient(circle, white 1px, transparent 1px) 0 0 / 4px 4px, + radial-gradient(circle, white 1px, transparent 1px) 100% 0 / 4px 4px, + radial-gradient(circle, white 1px, transparent 1px) 0 100% / 4px 4px, + radial-gradient(circle, white 1px, transparent 1px) 100% 100% / 4px 4px; + background-repeat: no-repeat; + pointer-events: none; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 3f7c164..109a82e 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,5 @@ import type { Metadata } from "next"; -import { Geist, Geist_Mono, Bebas_Neue } from "next/font/google"; +import { Geist, Geist_Mono, Bebas_Neue, Orbitron, Inter, JetBrains_Mono, Space_Mono, Rajdhani, Exo_2 } from "next/font/google"; import "./globals.css"; import { Navigation } from "@/components/Navigation"; import { Footer } from "@/components/Footer"; @@ -26,6 +26,51 @@ const bebasNeue = Bebas_Neue({ preload: true, }); +const orbitron = Orbitron({ + variable: "--font-orbitron", + subsets: ["latin"], + display: "swap", + preload: true, +}); + +const inter = Inter({ + variable: "--font-inter", + subsets: ["latin"], + display: "swap", + preload: true, +}); + +const jetbrainsMono = JetBrains_Mono({ + variable: "--font-jetbrains-mono", + subsets: ["latin"], + display: "swap", + preload: true, +}); + +const spaceMono = Space_Mono({ + variable: "--font-space-mono", + subsets: ["latin"], + weight: ["400", "700"], + display: "swap", + preload: true, +}); + +const rajdhani = Rajdhani({ + variable: "--font-rajdhani", + subsets: ["latin"], + weight: ["300", "400", "500", "600", "700"], + display: "swap", + preload: true, +}); + +const exo2 = Exo_2({ + variable: "--font-exo-2", + subsets: ["latin"], + weight: ["400", "700", "800", "900"], + display: "swap", + preload: true, +}); + export const metadata: Metadata = { title: "Biohazard VFX - Visual Effects Studio", description: "Creating stunning visual effects for commercials, music videos, and digital media. Expert VFX, motion graphics, compositing, and 3D animation services.", @@ -79,7 +124,7 @@ export default function RootLayout({ />
{children} diff --git a/src/components/CursorDotBackground.tsx b/src/components/CursorDotBackground.tsx new file mode 100644 index 0000000..56a995e --- /dev/null +++ b/src/components/CursorDotBackground.tsx @@ -0,0 +1,120 @@ +"use client"; + +import { useEffect, useState, useRef } from "react"; + +interface CursorDotBackgroundProps { + dotSize?: number; + dotSpacing?: number; + fadeDistance?: number; + opacity?: number; + className?: string; +} + +export function CursorDotBackground({ + dotSize = 1, + dotSpacing = 20, + fadeDistance = 100, + opacity = 0.3, + className = "", +}: CursorDotBackgroundProps) { + const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }); + const [isHovering, setIsHovering] = useState(false); + const containerRef = useRef(null); + + useEffect(() => { + const handleMouseMove = (e: MouseEvent) => { + if (containerRef.current) { + const rect = containerRef.current.getBoundingClientRect(); + setMousePosition({ + x: e.clientX - rect.left, + y: e.clientY - rect.top, + }); + } + }; + + const handleMouseEnter = () => setIsHovering(true); + const handleMouseLeave = () => setIsHovering(false); + + const container = containerRef.current; + if (container) { + container.addEventListener("mousemove", handleMouseMove); + container.addEventListener("mouseenter", handleMouseEnter); + container.addEventListener("mouseleave", handleMouseLeave); + } + + return () => { + if (container) { + container.removeEventListener("mousemove", handleMouseMove); + container.removeEventListener("mouseenter", handleMouseEnter); + container.removeEventListener("mouseleave", handleMouseLeave); + } + }; + }, []); + + // Generate dots based on container size + const generateDots = () => { + if (!containerRef.current) return []; + + const rect = containerRef.current.getBoundingClientRect(); + const width = rect.width; + const height = rect.height; + + const dots = []; + const cols = Math.ceil(width / dotSpacing); + const rows = Math.ceil(height / dotSpacing); + + for (let row = 0; row < rows; row++) { + for (let col = 0; col < cols; col++) { + const x = col * dotSpacing; + const y = row * dotSpacing; + + // Calculate distance from mouse position + const distance = Math.sqrt( + Math.pow(x - mousePosition.x, 2) + Math.pow(y - mousePosition.y, 2) + ); + + // Calculate opacity based on distance and hover state + let dotOpacity = 0; + if (isHovering && distance <= fadeDistance) { + dotOpacity = opacity * (1 - distance / fadeDistance); + } + + if (dotOpacity > 0) { + dots.push({ + x, + y, + opacity: dotOpacity, + }); + } + } + } + + return dots; + }; + + return ( +
+ {generateDots().map((dot, index) => ( +
+ ))} +
+ ); +} diff --git a/src/components/HorizontalAccordion.tsx b/src/components/HorizontalAccordion.tsx new file mode 100644 index 0000000..c64109a --- /dev/null +++ b/src/components/HorizontalAccordion.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { ChevronRight } from "lucide-react"; + +interface HorizontalAccordionProps { + trigger: string; + children: React.ReactNode; + className?: string; +} + +export function HorizontalAccordion({ + trigger, + children, + className = "" +}: HorizontalAccordionProps) { + const [isOpen, setIsOpen] = useState(false); + + return ( +
+ {/* Trigger Button */} + setIsOpen(!isOpen)} + className="flex items-center gap-2 text-gray-300 hover:text-white transition-colors whitespace-nowrap text-lg font-medium" + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + > + {trigger} + + + + + + {/* Animated Content */} + + {isOpen && ( + + + {children} + + + )} + +
+ ); +} diff --git a/src/components/Temp-Placeholder.tsx b/src/components/Temp-Placeholder.tsx index 0861f35..8e1f70f 100644 --- a/src/components/Temp-Placeholder.tsx +++ b/src/components/Temp-Placeholder.tsx @@ -1,67 +1,126 @@ +"use client"; + +import { CursorDotBackground } from "./CursorDotBackground"; +import { HorizontalAccordion } from "./HorizontalAccordion"; +import { useEffect, useRef, useState } from "react"; + export function TempPlaceholder() { + const titleRef = useRef(null); + const titleInnerRef = useRef(null); + const bioTextRef = useRef(null); + const [titleWidth, setTitleWidth] = useState(null); + const [bioFontSizePx, setBioFontSizePx] = useState(null); + const baseBioFontSizeRef = useRef(null); + + useEffect(() => { + const measure = () => { + const measuredTitleWidth = titleInnerRef.current?.offsetWidth ?? null; + setTitleWidth(measuredTitleWidth); + + if (measuredTitleWidth && bioTextRef.current) { + const element = bioTextRef.current; + if (baseBioFontSizeRef.current === null) { + const initialFontSize = parseFloat(getComputedStyle(element).fontSize); + baseBioFontSizeRef.current = isNaN(initialFontSize) ? 16 : initialFontSize; + } + + // Temporarily ensure we measure at base font size + const baseFontSize = baseBioFontSizeRef.current ?? 16; + const previousInlineFontSize = element.style.fontSize; + element.style.fontSize = `${baseFontSize}px`; + const bioNaturalWidth = element.offsetWidth; + // Restore previous inline style before we set state (will update after render) + element.style.fontSize = previousInlineFontSize; + + if (bioNaturalWidth > 0) { + const scale = measuredTitleWidth / bioNaturalWidth; + setBioFontSizePx(baseFontSize * scale); + } + } + }; + measure(); + window.addEventListener("resize", measure); + return () => window.removeEventListener("resize", measure); + }, []); return (
+
+ +

10-12-2025

-

- You've gotta be fucking me. +

+ + You've gotta be fucking me. +

This is the 20th fucking time this has happened.

-

+

Nicholai broke the website, again.

-
-
+
+ +
+

+ (TLDR: perfectionism is the mind killer) +

+
    +
  1. We needed a website (circa January 2023)
  2. +
  3. We tried to build one on squarespace (that shit sucks)
  4. +
  5. + Nicholai figured "I know some html and javascript, why not just{" "} + make one." +
  6. +
  7. + But of course, the html site sucked and was + difficult to host. +
  8. +
  9. + And naturally, the website for some reason needed to look + good. +
  10. +
  11. + So then began a longwinded journey of Nicholai learning react +
  12. +
  13. Nicholai should've stuck to python.
  14. +
+
+
+
-

- How did we get here? Lets break it down: -

-

- (TLDR: perfectionism is the mind killer) -

+

Anyway, heres all you assholes need for right now:

-
    -
  1. We needed a website (circa January 2023)
  2. -
  3. We tried to build one on squarespace (that shit sucks)
  4. -
  5. - Nicholai figured "I know some html and javascript, why not just{" "} - make one." -
  6. -
  7. - But of course, the html site sucked and was - difficult to host. -
  8. -
  9. - And naturally, the website for some reason needed to look - good. -
  10. -
  11. - So then began a longwinded journey of Nicholai learning react -
  12. -
  13. Nicholai should've stuck to python.
  14. -
- -
- -

Anyway, heres all you assholes need for right now:

- -

BIOHAZARD VFX

+

+ + BIOHAZARD + +

Who we are: artists and technical people, we're better at VFX than we are at web design, I promise.

-
-
-

Here's our reel:{" "} @@ -79,15 +138,12 @@ export function TempPlaceholder() { Some projects we've worked on:

-
-
-
+
); diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx new file mode 100644 index 0000000..2f55a32 --- /dev/null +++ b/src/components/ui/accordion.tsx @@ -0,0 +1,57 @@ +"use client" + +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDown } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)) +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx new file mode 100644 index 0000000..5afd41d --- /dev/null +++ b/src/components/ui/alert.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } diff --git a/src/components/ui/hover-card.tsx b/src/components/ui/hover-card.tsx new file mode 100644 index 0000000..74efa6b --- /dev/null +++ b/src/components/ui/hover-card.tsx @@ -0,0 +1,29 @@ +"use client" + +import * as React from "react" +import * as HoverCardPrimitive from "@radix-ui/react-hover-card" + +import { cn } from "@/lib/utils" + +const HoverCard = HoverCardPrimitive.Root + +const HoverCardTrigger = HoverCardPrimitive.Trigger + +const HoverCardContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + +)) +HoverCardContent.displayName = HoverCardPrimitive.Content.displayName + +export { HoverCard, HoverCardTrigger, HoverCardContent }