diff --git a/src/components/Hack/CollapsibleTags.tsx b/src/components/Hack/CollapsibleTags.tsx
new file mode 100644
index 0000000..1aa95c7
--- /dev/null
+++ b/src/components/Hack/CollapsibleTags.tsx
@@ -0,0 +1,97 @@
+"use client";
+
+import { useState, useRef, useLayoutEffect, useEffect, useCallback } from "react";
+import { FaChevronDown } from "react-icons/fa6";
+
+interface CollapsibleTagsProps {
+ tags: string[];
+}
+
+// Tag height: ~28px (px-2.5 py-1 text-xs), gap: 8px
+// Max height: 2.5 × 28px + 2 × 8px = 86px (rounded to 88px for safety)
+const MAX_HEIGHT = 72;
+
+export default function CollapsibleTags({ tags }: CollapsibleTagsProps) {
+ const [isExpanded, setIsExpanded] = useState(false);
+ const [needsExpansion, setNeedsExpansion] = useState(false);
+ const [naturalHeight, setNaturalHeight] = useState
(null);
+ const contentRef = useRef(null);
+
+ const checkIfExpansionNeeded = useCallback(() => {
+ if (!contentRef.current) return;
+ const height = contentRef.current.scrollHeight;
+ setNaturalHeight(height);
+ setNeedsExpansion(height > MAX_HEIGHT);
+ }, []);
+
+ // Use useLayoutEffect to check synchronously before paint
+ useLayoutEffect(() => {
+ checkIfExpansionNeeded();
+ }, [tags, checkIfExpansionNeeded]);
+
+ // Also check on window resize
+ useEffect(() => {
+ window.addEventListener("resize", checkIfExpansionNeeded);
+ return () => {
+ window.removeEventListener("resize", checkIfExpansionNeeded);
+ };
+ }, [checkIfExpansionNeeded]);
+
+ if (tags.length === 0) return null;
+
+ return (
+
+
+
+
+ {tags.map((t) => (
+
+ {t}
+
+ ))}
+
+
+
+ {needsExpansion && (
+
+ )}
+
+ );
+}