{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "content-05",
  "title": "Content 05",
  "description": "A focus grid with items that can be focused and dimmed.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/blocks/content/content-05/content-05.tsx",
      "content": "'use client'\n\nimport { AnimatePresence, motion } from 'motion/react'\nimport { useEffect, useState } from 'react'\n\nexport interface Content05Item {\n  image: string\n  title: string\n  description: string\n  defaultFocus?: boolean\n}\n\nexport interface Content05Props {\n  title?: string\n  description?: string\n  items: Content05Item[]\n  dimUnfocused?: boolean\n  descriptionOnFocus?: boolean\n}\n\n// ─── Constants ───────────────────────────────────────────────────────────────\n\nconst BREAKPOINTS = { md: 768, lg: 1024 } as const\nconst COLS = { sm: 1, md: 2, lg: 3 } as const\nconst GAP_REM = 1\n\n// ─── Hooks ───────────────────────────────────────────────────────────────────\n\nfunction useColsPerRow(): number | undefined {\n  const [cols, setCols] = useState<number | undefined>(undefined)\n\n  useEffect(() => {\n    const update = () => {\n      const width = window.innerWidth\n      if (width >= BREAKPOINTS.lg) setCols(COLS.lg)\n      else if (width >= BREAKPOINTS.md) setCols(COLS.md)\n      else setCols(COLS.sm)\n    }\n\n    update()\n    window.addEventListener('resize', update)\n    return () => window.removeEventListener('resize', update)\n  }, [])\n\n  return cols\n}\n\n// ─── Utilities ───────────────────────────────────────────────────────────────\n\nfunction chunk<T>(array: T[], size: number): T[][] {\n  return Array.from({ length: Math.ceil(array.length / size) }, (_, i) =>\n    array.slice(i * size, i * size + size),\n  )\n}\n\nfunction getItemWidth(\n  isFocused: boolean,\n  rowSize: number,\n  hasRowFocus: boolean,\n): string {\n  if (rowSize === 1) return '100%'\n  const gapTotal = (rowSize - 1) * GAP_REM\n  if (!hasRowFocus) return `calc((100% - ${gapTotal}rem) / ${rowSize})`\n  const focusedFraction = 2 / (rowSize + 1)\n  const otherFraction = (1 - focusedFraction) / (rowSize - 1)\n  const fraction = isFocused ? focusedFraction : otherFraction\n  return `calc((100% - ${gapTotal}rem) * ${fraction})`\n}\n\n// ─── ItemCard ─────────────────────────────────────────────────────────────────\n\ntype ItemCardProps = Readonly<{\n  item: Content05Item\n  isFocused?: boolean\n  dimmed?: boolean\n  showDescription?: boolean\n  width?: string\n  onActivate?: () => void\n}>\n\nfunction ItemCard({\n  item,\n  isFocused = false,\n  dimmed = false,\n  showDescription = true,\n  width,\n  onActivate,\n}: ItemCardProps) {\n  return (\n    <button\n      type=\"button\"\n      onClick={onActivate}\n      aria-pressed={isFocused}\n      className=\"group flex shrink-0 cursor-pointer flex-col rounded-xl text-left transition-[width,opacity] duration-500 ease-in-out focus:ring-0 focus:outline-none\"\n      style={{\n        ...(width !== undefined && { width }),\n        ...(dimmed && { opacity: 0.4 }),\n      }}\n    >\n      <div className=\"relative h-64 w-full\">\n        <img\n          src={item.image}\n          alt={item.title}\n          loading=\"lazy\"\n          decoding=\"async\"\n          className=\"group-focus-visible:ring-ring/50 absolute inset-0 size-full rounded-xl object-cover group-focus-visible:ring-2 group-focus-visible:outline-none\"\n        />\n      </div>\n      <div className=\"py-2\">\n        <h3 className=\"line-clamp-1 truncate font-semibold\">{item.title}</h3>\n        <AnimatePresence initial={false}>\n          {showDescription && (\n            <motion.div\n              key=\"description-wrapper\"\n              initial={{ height: 0 }}\n              animate={{ height: 'auto' }}\n              exit={{\n                height: 0,\n                transition: {\n                  duration: 0.15,\n                  ease: [0.22, 1, 0.36, 1],\n                },\n              }}\n              transition={{\n                duration: 0.35,\n                ease: [0.22, 1, 0.36, 1],\n                delay: 0.35,\n              }}\n              className=\"overflow-hidden\"\n            >\n              <motion.p\n                initial={{ opacity: 0, filter: 'blur(6px)' }}\n                animate={{ opacity: 1, filter: 'blur(0px)' }}\n                exit={{\n                  opacity: 0,\n                  filter: 'blur(6px)',\n                  transition: { duration: 0.5, ease: [0.22, 1, 0.36, 1] },\n                }}\n                transition={{\n                  duration: 0.35,\n                  ease: [0.22, 1, 0.36, 1],\n                  delay: 0.4,\n                }}\n                className=\"text-muted-foreground mt-1 line-clamp-1 text-sm\"\n              >\n                {item.description}\n              </motion.p>\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </div>\n    </button>\n  )\n}\n\n// ─── Content05 ────────────────────────────────────────────────────────────────\n\nexport function Content05({\n  title,\n  description,\n  items,\n  dimUnfocused = false,\n  descriptionOnFocus = false,\n}: Readonly<Content05Props>) {\n  const [focusedIndex, setFocusedIndex] = useState(() =>\n    items.findIndex((item) => item.defaultFocus),\n  )\n  const colsPerRow = useColsPerRow()\n\n  const isMobile = colsPerRow === COLS.sm\n  const rows = colsPerRow === undefined ? [] : chunk(items, colsPerRow)\n  const hasFocusedItem = focusedIndex !== -1\n\n  const toggle = (index: number) =>\n    setFocusedIndex((prev) => (prev === index ? -1 : index))\n\n  if (colsPerRow === undefined) return null\n\n  if (isMobile) {\n    return (\n      <div className=\"grid grid-cols-1 gap-4\">\n        {items.map((item) => (\n          <ItemCard key={item.title} item={item} />\n        ))}\n      </div>\n    )\n  }\n\n  return (\n    <div className=\"flex flex-col gap-4\">\n      {(title || description) && (\n        <div className=\"flex flex-col gap-1\">\n          {title && <h2 className=\"text-2xl font-bold\">{title}</h2>}\n          {description && (\n            <p className=\"text-muted-foreground\">{description}</p>\n          )}\n        </div>\n      )}\n      <div className=\"flex flex-col gap-4\">\n        {rows.map((row, rowIndex) => {\n          const rowStartIndex = rowIndex * colsPerRow\n          const hasRowFocus =\n            focusedIndex >= rowStartIndex &&\n            focusedIndex < rowStartIndex + row.length\n          return (\n            <div key={rowIndex} className=\"flex gap-4\">\n              {row.map((item, colIndex) => {\n                const globalIndex = rowStartIndex + colIndex\n                const isFocused = focusedIndex === globalIndex\n                return (\n                  <ItemCard\n                    key={item.title}\n                    item={item}\n                    isFocused={isFocused}\n                    dimmed={\n                      dimUnfocused &&\n                      hasFocusedItem &&\n                      focusedIndex !== globalIndex\n                    }\n                    showDescription={descriptionOnFocus ? isFocused : true}\n                    width={getItemWidth(isFocused, row.length, hasRowFocus)}\n                    onActivate={() => toggle(globalIndex)}\n                  />\n                )\n              })}\n            </div>\n          )\n        })}\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/flx/blocks/content/content-05/content-05.tsx"
    },
    {
      "path": "registry/blocks/content/content-05/content-05-example.tsx",
      "content": "import { Content05, type Content05Props } from './content-05'\n\nexport const values = {\n  title: 'Our process',\n  description: 'From idea to launch—click each phase to see how we work.',\n  dimUnfocused: false,\n  descriptionOnFocus: true,\n  items: [\n    {\n      image:\n        'https://images.unsplash.com/photo-1523275335684-37898b6baf30?q=80&w=1099&auto=format&fit=crop',\n      title: 'Discovery',\n      description:\n        'We start by understanding your goals, your users, and the constraints you face.',\n      defaultFocus: true,\n    },\n    {\n      image:\n        'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?q=80&w=1170&auto=format&fit=crop',\n      title: 'Design',\n      description:\n        'Clean interfaces and clear flows that people love to use every day.',\n    },\n    {\n      image:\n        'https://images.unsplash.com/photo-1587563871167-1ee9c731aefb?q=80&w=1131&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',\n      title: 'Build',\n      description:\n        'Solid, accessible code that scales with your product and your team.',\n    },\n    {\n      image:\n        'https://images.unsplash.com/photo-1713623069173-3afa89926882?q=80&w=1170&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',\n      title: 'Test',\n      description:\n        'We validate everything with real users before launch, so nothing is left to chance.',\n    },\n    {\n      image:\n        'https://images.unsplash.com/photo-1625245488600-f03fef636a3c?q=80&w=1074&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',\n      title: 'Launch',\n      description:\n        'Go live with confidence, with support and iteration when you need it.',\n    },\n  ],\n} satisfies Content05Props\n\nexport function Content05Example() {\n  return (\n    <Content05\n      title={values.title}\n      description={values.description}\n      dimUnfocused={values.dimUnfocused}\n      descriptionOnFocus={values.descriptionOnFocus}\n      items={values.items}\n    />\n  )\n}\n",
      "type": "registry:component",
      "target": "components/flx/blocks/content/content-05/content-05-example.tsx"
    }
  ],
  "meta": {
    "iframeHeight": 800
  },
  "type": "registry:block"
}